Add an iced peak meter widget
This commit is contained in:
parent
d830a0a1e4
commit
4d58df1e08
|
@ -8,9 +8,11 @@
|
|||
use nih_plug::param::internals::ParamPtr;
|
||||
|
||||
pub mod param_slider;
|
||||
pub mod peak_meter;
|
||||
pub mod util;
|
||||
|
||||
pub use param_slider::ParamSlider;
|
||||
pub use peak_meter::PeakMeter;
|
||||
|
||||
/// A message to update a parameter value. Since NIH-plug manages the parameters, interacting with
|
||||
/// parameter values with iced works a little different from updating any other state. This main
|
||||
|
|
300
nih_plug_iced/src/widgets/peak_meter.rs
Normal file
300
nih_plug_iced/src/widgets/peak_meter.rs
Normal file
|
@ -0,0 +1,300 @@
|
|||
//! A super simple peak meter widget.
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::backend::Renderer;
|
||||
use crate::renderer::Renderer as GraphicsRenderer;
|
||||
use crate::text::Renderer as TextRenderer;
|
||||
use crate::{
|
||||
alignment, layout, renderer, text, Background, Color, Element, Font, Layout, Length, Point,
|
||||
Rectangle, Size, Widget,
|
||||
};
|
||||
|
||||
/// The thickness of this widget's borders.
|
||||
const BORDER_WIDTH: f32 = 1.0;
|
||||
/// The thickness of a tick inside of the peak meter's bar.
|
||||
const TICK_WIDTH: f32 = 1.0;
|
||||
|
||||
/// A simple horizontal peak meter.
|
||||
///
|
||||
/// TODO: There are currently no styling options at all
|
||||
/// TODO: Vertical peak meter, this is just a proof of concept to fit the gain GUI example.
|
||||
pub struct PeakMeter<'a, Message> {
|
||||
state: &'a mut State,
|
||||
|
||||
/// The current measured value in decibel.
|
||||
current_value_db: f32,
|
||||
|
||||
/// The time the old peak value should remain visible.
|
||||
hold_time: Option<Duration>,
|
||||
|
||||
height: Length,
|
||||
width: Length,
|
||||
text_size: Option<u16>,
|
||||
font: Font,
|
||||
|
||||
/// We don't emit any messages, but iced requires us to define some message type anyways.
|
||||
_phantom: PhantomData<Message>,
|
||||
}
|
||||
|
||||
/// State for a [`PeakMeter`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State {
|
||||
/// The last peak value in decibel.
|
||||
held_peak_value_db: AtomicCell<f32>,
|
||||
/// When the last peak value was hit.
|
||||
last_held_peak_value: AtomicCell<Option<Instant>>,
|
||||
}
|
||||
|
||||
impl<'a, Message> PeakMeter<'a, Message> {
|
||||
/// Creates a new [`PeakMeter`] using the current measurement in decibel. This measurement can
|
||||
/// already have some form of smoothing applied to it. This peak slider widget can draw the last
|
||||
/// hold value for you.
|
||||
pub fn new(state: &'a mut State, value_db: f32) -> Self {
|
||||
Self {
|
||||
state,
|
||||
|
||||
current_value_db: value_db,
|
||||
|
||||
hold_time: None,
|
||||
|
||||
width: Length::Units(180),
|
||||
height: Length::Units(30),
|
||||
text_size: None,
|
||||
font: <Renderer as TextRenderer>::Font::default(),
|
||||
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep showing the peak value for a certain amount of time.
|
||||
pub fn hold_time(mut self, time: Duration) -> Self {
|
||||
self.hold_time = Some(time);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PeakMeter`].
|
||||
pub fn width(mut self, width: Length) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`PeakMeter`].
|
||||
pub fn height(mut self, height: Length) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`PeakMeter`]'s ticks bar.
|
||||
pub fn text_size(mut self, size: u16) -> Self {
|
||||
self.text_size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`PeakMeter`]'s ticks bar.
|
||||
pub fn font(mut self, font: Font) -> Self {
|
||||
self.font = font;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Widget<Message, Renderer> for PeakMeter<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(&self, _renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let bar_bounds = Rectangle {
|
||||
height: bounds.height / 2.0,
|
||||
..bounds
|
||||
};
|
||||
let ticks_bounds = Rectangle {
|
||||
y: bounds.y + (bounds.height / 2.0),
|
||||
height: bounds.height / 2.0,
|
||||
..bounds
|
||||
};
|
||||
|
||||
let text_size = self
|
||||
.text_size
|
||||
.unwrap_or_else(|| (renderer.default_size() as f32 * 0.7).round() as u16);
|
||||
|
||||
// We'll draw a simple horizontal for [-200, 20] dB where we'll treat -80 as -infinity, with
|
||||
// a label containing the tick markers below it. If `.hold_time()` was called then we'll
|
||||
// also display the last held value
|
||||
const MIN_TICK: f32 = -100.0;
|
||||
const MAX_TICK: f32 = 20.0;
|
||||
let text_ticks = [-80i32, -60, -40, -20, 0];
|
||||
// Draw a tick with one pixel in between, otherwise the bilinear interpolation makes
|
||||
// everything a smeary mess
|
||||
let bar_ticks_start = (bar_bounds.x + BORDER_WIDTH).round() as i32;
|
||||
let bar_ticks_end = (bar_bounds.x + bar_bounds.width - (BORDER_WIDTH * 2.0)).ceil() as i32;
|
||||
let bar_tick_coordinates =
|
||||
(bar_ticks_start..bar_ticks_end).step_by((TICK_WIDTH + 1.0).round() as usize);
|
||||
let db_to_x_coord = |db: f32| {
|
||||
let tick_fraction = (db - MIN_TICK) / (MAX_TICK - MIN_TICK);
|
||||
bar_ticks_start as f32
|
||||
+ ((bar_ticks_end - bar_ticks_start) as f32 * tick_fraction).round()
|
||||
};
|
||||
|
||||
for tick_x in bar_tick_coordinates {
|
||||
let tick_fraction =
|
||||
(tick_x - bar_ticks_start) as f32 / (bar_ticks_end - bar_ticks_start) as f32;
|
||||
let tick_db = (tick_fraction * (MAX_TICK - MIN_TICK)) + MIN_TICK;
|
||||
if tick_db > self.current_value_db {
|
||||
break;
|
||||
}
|
||||
|
||||
let tick_bounds = Rectangle {
|
||||
x: tick_x as f32,
|
||||
y: bar_bounds.y + BORDER_WIDTH,
|
||||
width: TICK_WIDTH,
|
||||
height: bar_bounds.height - (BORDER_WIDTH * 2.0),
|
||||
};
|
||||
|
||||
let grayscale_color = 0.3 + ((1.0 - tick_fraction) * 0.5);
|
||||
let tick_color = Color::from_rgb(grayscale_color, grayscale_color, grayscale_color);
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: tick_bounds,
|
||||
border_radius: 0.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(tick_color),
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the hold peak value if the hold time option has been set
|
||||
if let Some(hold_time) = self.hold_time {
|
||||
let now = Instant::now();
|
||||
let mut held_peak_value_db = self.state.held_peak_value_db.load();
|
||||
let last_peak_value = self.state.last_held_peak_value.load();
|
||||
if self.current_value_db >= held_peak_value_db
|
||||
|| last_peak_value.is_none()
|
||||
|| now > last_peak_value.unwrap() + hold_time
|
||||
{
|
||||
self.state.held_peak_value_db.store(self.current_value_db);
|
||||
self.state.last_held_peak_value.store(Some(now));
|
||||
held_peak_value_db = self.current_value_db;
|
||||
}
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: db_to_x_coord(held_peak_value_db),
|
||||
y: bar_bounds.y + BORDER_WIDTH,
|
||||
width: TICK_WIDTH,
|
||||
height: bar_bounds.height - (BORDER_WIDTH * 2.0),
|
||||
},
|
||||
border_radius: 0.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(Color::from_rgb(0.3, 0.3, 0.3)),
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the bar after the ticks since the first and last tick may overlap with the borders
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: bar_bounds,
|
||||
border_radius: 0.0,
|
||||
border_width: BORDER_WIDTH,
|
||||
border_color: Color::BLACK,
|
||||
},
|
||||
Background::Color(Color::TRANSPARENT),
|
||||
);
|
||||
|
||||
// Beneat the bar we want to draw the names of the ticks
|
||||
for tick_db in text_ticks {
|
||||
let x_coordinate = db_to_x_coord(tick_db as f32);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: x_coordinate,
|
||||
y: ticks_bounds.y,
|
||||
width: TICK_WIDTH,
|
||||
height: ticks_bounds.height * 0.3,
|
||||
},
|
||||
border_radius: 0.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(Color::from_rgb(0.3, 0.3, 0.3)),
|
||||
);
|
||||
|
||||
let tick_text = if tick_db == text_ticks[0] {
|
||||
String::from("-inf")
|
||||
} else {
|
||||
tick_db.to_string()
|
||||
};
|
||||
renderer.fill_text(text::Text {
|
||||
content: &tick_text,
|
||||
font: self.font,
|
||||
size: text_size as f32,
|
||||
bounds: Rectangle {
|
||||
x: x_coordinate,
|
||||
y: ticks_bounds.y + (ticks_bounds.height * 0.35),
|
||||
..ticks_bounds
|
||||
},
|
||||
color: style.text_color,
|
||||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
});
|
||||
}
|
||||
|
||||
// Every proper graph needs a unit label
|
||||
let zero_db_x_coordinate = db_to_x_coord(0.0);
|
||||
let zero_db_text_width = renderer.measure_width("0", text_size, self.font);
|
||||
renderer.fill_text(text::Text {
|
||||
// The spacing looks a bit off if we start with a space here so we'll add a little
|
||||
// offset to the x-coordinate instead
|
||||
content: "dBFS",
|
||||
font: self.font,
|
||||
size: text_size as f32,
|
||||
bounds: Rectangle {
|
||||
x: zero_db_x_coordinate + (zero_db_text_width / 2.0) + (text_size as f32 * 0.2),
|
||||
y: ticks_bounds.y + (ticks_bounds.height * 0.35),
|
||||
..ticks_bounds
|
||||
},
|
||||
color: style.text_color,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> From<PeakMeter<'a, Message>> for Element<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
fn from(widget: PeakMeter<'a, Message>) -> Self {
|
||||
Element::new(widget)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
use nih_plug::prelude::{Editor, GuiContext};
|
||||
use atomic_float::AtomicF32;
|
||||
use nih_plug::prelude::{util, Editor, GuiContext};
|
||||
use nih_plug_iced::widgets as nih_widgets;
|
||||
use nih_plug_iced::*;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::GainParams;
|
||||
|
||||
|
@ -13,17 +15,20 @@ pub(crate) fn default_state() -> Arc<IcedState> {
|
|||
|
||||
pub(crate) fn create(
|
||||
params: Pin<Arc<GainParams>>,
|
||||
peak_meter: Arc<AtomicF32>,
|
||||
editor_state: Arc<IcedState>,
|
||||
) -> Option<Box<dyn Editor>> {
|
||||
create_iced_editor::<GainEditor>(editor_state, params)
|
||||
create_iced_editor::<GainEditor>(editor_state, (params, peak_meter))
|
||||
}
|
||||
|
||||
struct GainEditor {
|
||||
params: Pin<Arc<GainParams>>,
|
||||
context: Arc<dyn GuiContext>,
|
||||
|
||||
peak_meter: Arc<AtomicF32>,
|
||||
|
||||
gain_slider_state: nih_widgets::param_slider::State,
|
||||
meter_dummy_state: widget::button::State,
|
||||
peak_meter_state: nih_widgets::peak_meter::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -35,18 +40,20 @@ enum Message {
|
|||
impl IcedEditor for GainEditor {
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type InitializationFlags = Pin<Arc<GainParams>>;
|
||||
type InitializationFlags = (Pin<Arc<GainParams>>, Arc<AtomicF32>);
|
||||
|
||||
fn new(
|
||||
params: Self::InitializationFlags,
|
||||
(params, peak_meter): Self::InitializationFlags,
|
||||
context: Arc<dyn GuiContext>,
|
||||
) -> (Self, Command<Self::Message>) {
|
||||
let editor = GainEditor {
|
||||
params,
|
||||
context,
|
||||
|
||||
peak_meter,
|
||||
|
||||
gain_slider_state: Default::default(),
|
||||
meter_dummy_state: widget::button::State::new(),
|
||||
peak_meter_state: Default::default(),
|
||||
};
|
||||
|
||||
(editor, Command::none())
|
||||
|
@ -88,24 +95,20 @@ impl IcedEditor for GainEditor {
|
|||
.vertical_alignment(alignment::Vertical::Center),
|
||||
)
|
||||
.push(
|
||||
nih_widgets::ParamSlider::new(&mut self.gain_slider_state, &self.params.gain, self.context.as_ref()).map(Message::ParamUpdate)
|
||||
// Button::new(&mut self.gain_dummy_state, Text::new("Gain"))
|
||||
// .height(30.into())
|
||||
// .width(180.into()),
|
||||
nih_widgets::ParamSlider::new(
|
||||
&mut self.gain_slider_state,
|
||||
&self.params.gain,
|
||||
self.context.as_ref(),
|
||||
)
|
||||
.map(Message::ParamUpdate),
|
||||
)
|
||||
.push(Space::with_height(10.into()))
|
||||
.push(
|
||||
Button::new(&mut self.meter_dummy_state, Text::new("Meter"))
|
||||
.height(15.into())
|
||||
.width(180.into()),
|
||||
)
|
||||
.push(
|
||||
Text::new("Ticks 'n stuff")
|
||||
.size(12)
|
||||
.height(15.into())
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.vertical_alignment(alignment::Vertical::Center),
|
||||
nih_widgets::PeakMeter::new(
|
||||
&mut self.peak_meter_state,
|
||||
util::gain_to_db(self.peak_meter.load(std::sync::atomic::Ordering::Relaxed)),
|
||||
)
|
||||
.hold_time(Duration::from_millis(600)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -25,10 +25,6 @@ struct Gain {
|
|||
struct GainParams {
|
||||
#[id = "gain"]
|
||||
pub gain: FloatParam,
|
||||
|
||||
// TODO: Remove this parameter when we're done implementing the widgets
|
||||
#[id = "foobar"]
|
||||
pub some_int: IntParam,
|
||||
}
|
||||
|
||||
impl Default for Gain {
|
||||
|
@ -57,7 +53,6 @@ impl Default for GainParams {
|
|||
.with_smoother(SmoothingStyle::Linear(50.0))
|
||||
.with_step_size(0.01)
|
||||
.with_unit(" dB"),
|
||||
some_int: IntParam::new("Something", 3, IntRange::Linear { min: 0, max: 3 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +76,11 @@ impl Plugin for Gain {
|
|||
}
|
||||
|
||||
fn editor(&self) -> Option<Box<dyn Editor>> {
|
||||
editor::create(self.params.clone(), self.editor_state.clone())
|
||||
editor::create(
|
||||
self.params.clone(),
|
||||
self.peak_meter.clone(),
|
||||
self.editor_state.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn accepts_bus_config(&self, config: &BusConfig) -> bool {
|
||||
|
|
Loading…
Reference in a new issue