diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 913db05..bdc4eea 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -66,6 +66,32 @@ impl ValueScaling { } .clamp(0., 1.) } + + pub fn value_to_normalized_optional(&self, value: f32, min: f32, max: f32) -> Option { + let unmap = |x: f32| -> f32 { (x - min) / (max - min) }; + + let value = match self { + ValueScaling::Linear => unmap(value), + + ValueScaling::Power(exponent) => unmap(value).powf(1.0 / *exponent), + + ValueScaling::Frequency => { + let minl = min.log2(); + let range = max.log2() - minl; + (value.log2() - minl) / range + } + + ValueScaling::Decibels => unmap({ + const CONVERSION_FACTOR: f32 = std::f32::consts::LOG10_E * 20.0; + value.ln() * CONVERSION_FACTOR + }), + }; + if (0.0..=1.0).contains(&value) { + Some(value) + } else { + None + } + } } // We can't use impl_res_simple!() since we're using nih_plug's version of VIZIA diff --git a/src/visualizers/graph.rs b/src/visualizers/graph.rs index be7ec35..4ac2d32 100644 --- a/src/visualizers/graph.rs +++ b/src/visualizers/graph.rs @@ -1,12 +1,7 @@ +use super::{FillFrom, FillModifiers, RangeModifiers}; use crate::utils::{ValueScaling, VisualizerBuffer}; -use nih_plug_vizia::vizia::style::Length::Value; -use nih_plug_vizia::vizia::{ - binding::{Lens, LensExt, Res}, - context::{Context, DrawContext}, - vg, - view::{Canvas, Handle, View}, -}; +use nih_plug_vizia::vizia::{prelude::*, vg}; use std::sync::{Arc, Mutex}; /// Real-time graph displaying information that is stored inside a buffer @@ -33,9 +28,14 @@ where I: VisualizerBuffer + 'static, { buffer: L, - display_range: (f32, f32), + range: (f32, f32), scaling: ValueScaling, - fill_from: f32, + fill_from: FillFrom, +} + +enum GraphEvents { + UpdateRange((f32, f32)), + UpdateScaling(ValueScaling), } impl Graph @@ -46,17 +46,18 @@ where pub fn new( cx: &mut Context, buffer: L, - display_range: impl Res<(f32, f32)>, - scaling: impl Res, + range: impl Res<(f32, f32)> + Clone, + scaling: impl Res + Clone, ) -> Handle { - let range = display_range.get_val(cx); Self { buffer, - display_range: range, + range: range.get_val(cx), scaling: scaling.get_val(cx), - fill_from: range.0, + fill_from: FillFrom::Bottom, } .build(cx, |_| {}) + .range(range) + .scaling(scaling) } } @@ -68,6 +69,12 @@ where fn element(&self) -> Option<&'static str> { Some("graph") } + fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { + event.map(|e, _| match e { + GraphEvents::UpdateRange(v) => self.range = *v, + GraphEvents::UpdateScaling(s) => self.scaling = *s, + }); + } fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { let bounds = cx.bounds(); @@ -82,21 +89,17 @@ where let binding = self.buffer.get(cx); let ring_buf = &(binding.lock().unwrap()); - let mut peak = self.scaling.value_to_normalized( - ring_buf[0], - self.display_range.0, - self.display_range.1, - ); + let mut peak = self + .scaling + .value_to_normalized(ring_buf[0], self.range.0, self.range.1); stroke.move_to(x, y + h * (1. - peak)); for i in 1..ring_buf.len() { // Normalize peak value - peak = self.scaling.value_to_normalized( - ring_buf[i], - self.display_range.0, - self.display_range.1, - ); + peak = self + .scaling + .value_to_normalized(ring_buf[i], self.range.0, self.range.1); // Draw peak as a new point stroke.line_to( @@ -106,12 +109,13 @@ where } let mut fill = stroke.clone(); - let fill_from_n = 1.0 - - ValueScaling::Linear.value_to_normalized( - self.fill_from, - self.display_range.0, - self.display_range.1, - ); + let fill_from_n = match self.fill_from { + FillFrom::Top => 0.0, + FillFrom::Bottom => 1.0, + FillFrom::Value(val) => { + 1.0 - ValueScaling::Linear.value_to_normalized(val, self.range.0, self.range.1) + } + }; fill.line_to(x + w, y + h * fill_from_n); fill.line_to(x, y + h * fill_from_n); @@ -126,7 +130,11 @@ where } } -pub trait GraphModifiers { +impl<'a, L, I> FillModifiers for Handle<'a, Graph> +where + L: Lens>>, + I: VisualizerBuffer + 'static, +{ /// Allows for the graph to be filled from the top instead of the bottom. /// /// This is useful for certain graphs like gain reduction meters. @@ -139,12 +147,15 @@ pub trait GraphModifiers { /// /// ``` /// Graph::new(cx, Data::gain_mult, (-32.0, 8.0), ValueScaling::Decibels) - /// .should_fill_from_top(true) + /// .fill_from_max() /// .color(Color::rgba(255, 0, 0, 160)) /// .background_color(Color::rgba(255, 0, 0, 60)); /// ``` - fn should_fill_from_top(self, fill_from_top: bool) -> Self; - + fn fill_from_max(self) -> Self { + self.modify(|graph| { + graph.fill_from = FillFrom::Top; + }) + } /// Allows for the graph to be filled from any desired level. /// /// This is useful for certain graphs like gain reduction meters. @@ -157,30 +168,38 @@ pub trait GraphModifiers { /// /// ``` /// Graph::new(cx, Data::gain_mult, (-32.0, 6.0), ValueScaling::Decibels) - /// .fill_from(1.0) + /// .fill_from(0.0) // Fills the graph from 0.0dB downwards /// .color(Color::rgba(255, 0, 0, 160)) /// .background_color(Color::rgba(255, 0, 0, 60)); /// ``` - fn fill_from(self, level: f32) -> Self; + fn fill_from_value(self, level: f32) -> Self { + self.modify(|graph| { + graph.fill_from = FillFrom::Value(level); + }) + } } -impl<'a, L, I> GraphModifiers for Handle<'a, Graph> +impl<'a, L, I> RangeModifiers for Handle<'a, Graph> where L: Lens>>, I: VisualizerBuffer + 'static, { - fn should_fill_from_top(self, fill_from_top: bool) -> Self { - self.modify(|graph| { - graph.fill_from = if fill_from_top { - graph.display_range.1 - } else { - graph.display_range.0 - }; - }) + fn range(mut self, range: impl Res<(f32, f32)>) -> Self { + let e = self.entity(); + + range.set_or_bind(self.context(), e, move |cx, r| { + (*cx).emit_to(e, GraphEvents::UpdateRange(r.clone())); + }); + + self } - fn fill_from(self, level: f32) -> Self { - self.modify(|graph| { - graph.fill_from = level; - }) + fn scaling(mut self, scaling: impl Res) -> Self { + let e = self.entity(); + + scaling.set_or_bind(self.context(), e, move |cx, s| { + (*cx).emit_to(e, GraphEvents::UpdateScaling(s.clone())) + }); + + self } } diff --git a/src/visualizers/grid.rs b/src/visualizers/grid.rs index ac4e7b9..3b27cf7 100644 --- a/src/visualizers/grid.rs +++ b/src/visualizers/grid.rs @@ -1,13 +1,9 @@ -use nih_plug_vizia::vizia::{ - binding::Res, - context::{Context, DrawContext}, - vg, - view::{Canvas, Handle, View}, - views::Orientation, -}; +use nih_plug_vizia::vizia::{prelude::*, vg}; use crate::utils::ValueScaling; +use super::RangeModifiers; + /// Generic grid backdrop that displays either horizontal or vertical lines. /// /// Put this grid inside a ZStack, along with your visualizer of choice. @@ -43,6 +39,11 @@ pub struct Grid { orientation: Orientation, } +enum GridEvents { + UpdateRange((f32, f32)), + UpdateScaling(ValueScaling), +} + impl Grid { pub fn new( cx: &mut Context, @@ -58,6 +59,8 @@ impl Grid { orientation, } .build(cx, |_| {}) + .range(range) + .scaling(scaling) } } @@ -115,4 +118,31 @@ impl View for Grid { &vg::Paint::color(cx.font_color().into()).with_line_width(line_width), ); } + fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { + event.map(|e, _| match e { + GridEvents::UpdateRange(v) => self.range = *v, + GridEvents::UpdateScaling(v) => self.scaling = *v, + }); + } +} + +impl<'a> RangeModifiers for Handle<'a, Grid> { + fn range(mut self, range: impl Res<(f32, f32)>) -> Self { + let e = self.entity(); + + range.set_or_bind(self.context(), e, move |cx, r| { + (*cx).emit_to(e, GridEvents::UpdateRange(r.clone())); + }); + + self + } + fn scaling(mut self, scaling: impl Res) -> Self { + let e = self.entity(); + + scaling.set_or_bind(self.context(), e, move |cx, s| { + (*cx).emit_to(e, GridEvents::UpdateScaling(s)); + }); + + self + } } diff --git a/src/visualizers/meter.rs b/src/visualizers/meter.rs index d80d925..1f27636 100644 --- a/src/visualizers/meter.rs +++ b/src/visualizers/meter.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Mutex}; use nih_plug_vizia::vizia::{prelude::*, vg}; +use super::{FillFrom, FillModifiers, RangeModifiers}; use crate::utils::ValueScaling; use crate::utils::VisualizerBuffer; @@ -29,8 +30,9 @@ where I: VisualizerBuffer + 'static, { buffer: L, - display_range: (f32, f32), + range: (f32, f32), scaling: ValueScaling, + fill_from: FillFrom, orientation: Orientation, } @@ -42,20 +44,28 @@ where pub fn new( cx: &mut Context, buffer: L, - display_range: impl Res<(f32, f32)>, + range: impl Res<(f32, f32)>, scaling: impl Res, orientation: Orientation, ) -> Handle { Self { buffer, - display_range: display_range.get_val(cx), + range: range.get_val(cx), scaling: scaling.get_val(cx), + fill_from: FillFrom::Bottom, orientation, } .build(cx, |_| {}) + .range(range) + .scaling(scaling) } } +enum MeterEvents { + UpdateRange((f32, f32)), + UpdateScaling(ValueScaling), +} + impl View for Meter where L: Lens>>, @@ -77,8 +87,8 @@ where let level = self.scaling.value_to_normalized( ring_buf[ring_buf.len() - 1], - self.display_range.0, - self.display_range.1, + self.range.0, + self.range.1, ); let mut path = vg::Path::new(); @@ -91,8 +101,20 @@ where outline.close(); canvas.fill_path(&outline, &vg::Paint::color(cx.font_color().into())); - path.line_to(x + w, y + h); - path.line_to(x, y + h); + let fill_from_n = match self.fill_from { + FillFrom::Top => 0.0, + FillFrom::Bottom => 1.0, + FillFrom::Value(val) => { + 1.0 - ValueScaling::Linear.value_to_normalized( + val, + self.range.0, + self.range.1, + ) + } + }; + + path.line_to(x + w, y + h * fill_from_n); + path.line_to(x, y + h * fill_from_n); path.close(); canvas.fill_path(&path, &vg::Paint::color(cx.background_color().into())); @@ -105,12 +127,100 @@ where outline.close(); canvas.fill_path(&outline, &vg::Paint::color(cx.font_color().into())); - path.line_to(x, y + h); - path.line_to(x, y); + let fill_from_n = match self.fill_from { + FillFrom::Top => 1.0, + FillFrom::Bottom => 0.0, + FillFrom::Value(val) => { + ValueScaling::Linear.value_to_normalized(val, self.range.0, self.range.1) + } + }; + + path.line_to(x + w * fill_from_n, y + h); + path.line_to(x + w * fill_from_n, y); path.close(); canvas.fill_path(&path, &vg::Paint::color(cx.background_color().into())); } }; } + fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { + event.map(|e, _| match e { + MeterEvents::UpdateRange(v) => self.range = *v, + MeterEvents::UpdateScaling(v) => self.scaling = *v, + }); + } +} + +impl<'a, L, I> FillModifiers for Handle<'a, Meter> +where + L: Lens>>, + I: VisualizerBuffer + 'static, +{ + /// Allows for the meter to be filled from the maximum instead of the minimum value. + /// + /// This is useful for certain meters like gain reduction meters. + /// + /// # Example + /// + /// Here's a gain reduction meter, which you could overlay on top of a peak meter. + /// + /// Here, `gain_mult` could be a [`MinimaBuffer`](crate::utils::MinimaBuffer). + /// + /// ``` + /// Meter::new(cx, Data::gain_mult, (-32.0, 8.0), ValueScaling::Decibels, Orientation::Vertical) + /// .fill_from_max() + /// .color(Color::rgba(255, 0, 0, 160)) + /// .background_color(Color::rgba(255, 0, 0, 60)); + /// ``` + fn fill_from_max(self) -> Self { + self.modify(|meter| { + meter.fill_from = FillFrom::Top; + }) + } + /// Allows for the meter to be filled from any desired level. + /// + /// This is useful for certain meters like gain reduction meters. + /// + /// # Example + /// + /// Here's a gain reduction meter, which you could overlay on top of a peak meter. + /// + /// Here, `gain_mult` could be a [`MinimaBuffer`](crate::utils::MinimaBuffer). + /// + /// ``` + /// Meter::new(cx, Data::gain_mult, (-32.0, 6.0), ValueScaling::Decibels, Orientation::Vertical) + /// .fill_from(0.0) // Fills the meter from 0.0dB downwards + /// .color(Color::rgba(255, 0, 0, 160)) + /// .background_color(Color::rgba(255, 0, 0, 60)); + /// ``` + fn fill_from_value(self, level: f32) -> Self { + self.modify(|meter| { + meter.fill_from = FillFrom::Value(level); + }) + } +} + +impl<'a, L, I> RangeModifiers for Handle<'a, Meter> +where + L: Lens>>, + I: VisualizerBuffer + 'static, +{ + fn range(mut self, range: impl Res<(f32, f32)>) -> Self { + let e = self.entity(); + + range.set_or_bind(self.context(), e, move |cx, r| { + (*cx).emit_to(e, MeterEvents::UpdateRange(r)); + }); + + self + } + fn scaling(mut self, scaling: impl Res) -> Self { + let e = self.entity(); + + scaling.set_or_bind(self.context(), e, move |cx, s| { + (*cx).emit_to(e, MeterEvents::UpdateScaling(s)); + }); + + self + } } diff --git a/src/visualizers/mod.rs b/src/visualizers/mod.rs index 0531064..58af505 100644 --- a/src/visualizers/mod.rs +++ b/src/visualizers/mod.rs @@ -17,3 +17,31 @@ pub use oscilloscope::*; pub use spectrum_analyzer::*; pub use unit_ruler::*; pub use waveform::*; + +use super::utils::ValueScaling; +use nih_plug_vizia::vizia::binding::Res; + +pub trait RangeModifiers { + /// Sets the minimum and maximum values that can be displayed by the view + /// + /// The values are relative to the scaling - e.g. for peak volume information, + /// `(-48., 6.)` would be -48 to +6 dB when the scaling is set to + /// [`ValueScaling::Decibels`] + fn range(self, range: impl Res<(f32, f32)>) -> Self; + /// Specifies what scaling the view should use + fn scaling(self, scaling: impl Res) -> Self; +} + +pub(crate) enum FillFrom { + Top, + Bottom, + Value(f32), +} + +pub trait FillModifiers { + /// Allows for the view to be filled from the max instead of the min value. + fn fill_from_max(self) -> Self; + + /// Allows for the view to be filled from any desired level. + fn fill_from_value(self, level: f32) -> Self; +} diff --git a/src/visualizers/oscilloscope.rs b/src/visualizers/oscilloscope.rs index 1e2ab3a..1462962 100644 --- a/src/visualizers/oscilloscope.rs +++ b/src/visualizers/oscilloscope.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Mutex}; use nih_plug_vizia::vizia::{prelude::*, vg}; +use super::RangeModifiers; use crate::utils::{ValueScaling, VisualizerBuffer, WaveformBuffer}; /// Waveform display for real-time input. @@ -29,16 +30,21 @@ where B: Lens>>, { buffer: B, - display_range: (f32, f32), + range: (f32, f32), scaling: ValueScaling, } +enum OscilloscopeEvents { + UpdateRange((f32, f32)), + UpdateScaling(ValueScaling), +} + impl Oscilloscope where B: Lens>>, { /// Creates a new Oscilloscope. - /// + /// /// Takes in a `buffer`, which should be used to store the peak values. You /// need to write to it inside your plugin code, thread-safely send it to /// the editor thread, and then pass it into this oscilloscope. Which is @@ -46,15 +52,17 @@ where pub fn new( cx: &mut Context, buffer: B, - display_range: impl Res<(f32, f32)>, + range: impl Res<(f32, f32)>, scaling: impl Res, ) -> Handle { Self { buffer, - display_range: display_range.get_val(cx), + range: range.get_val(cx), scaling: scaling.get_val(cx), } .build(cx, |_| {}) + .range(range) + .scaling(scaling) } } @@ -81,18 +89,14 @@ where let width_delta = w / ring_buf.len() as f32; // Local minima (bottom part of waveform) - let mut py = self.scaling.value_to_normalized( - ring_buf[0].0, - self.display_range.0, - self.display_range.1, - ); + let mut py = self + .scaling + .value_to_normalized(ring_buf[0].0, self.range.0, self.range.1); fill.move_to(x, y + h * (1. - py) + 1.); for i in 1..ring_buf.len() { - py = self.scaling.value_to_normalized( - ring_buf[i].0, - self.display_range.0, - self.display_range.1, - ); + py = self + .scaling + .value_to_normalized(ring_buf[i].0, self.range.0, self.range.1); fill.line_to(x + width_delta * i as f32, y + h * (1. - py) + 1.); } @@ -103,16 +107,16 @@ where // Local maxima (top part of waveform) py = self.scaling.value_to_normalized( ring_buf[ring_buf.len() - 1].1, - self.display_range.0, - self.display_range.1, + self.range.0, + self.range.1, ); fill.line_to(x + w, y + h * (1. - py) + 1.); top_stroke.move_to(x + w, y + h * (1. - py) + 1.); for i in 1..ring_buf.len() { py = self.scaling.value_to_normalized( ring_buf[ring_buf.len() - i].1, - self.display_range.0, - self.display_range.1, + self.range.0, + self.range.1, ); fill.line_to(x + w - width_delta * i as f32, y + h * (1. - py) + 1.); @@ -125,4 +129,34 @@ where &vg::Paint::color(cx.font_color().into()).with_line_width(0.), ); } + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { + event.map(|e, _| match e { + OscilloscopeEvents::UpdateRange(v) => self.range = *v, + OscilloscopeEvents::UpdateScaling(v) => self.scaling = *v, + }); + } +} + +impl<'a, B> RangeModifiers for Handle<'a, Oscilloscope> +where + B: Lens>>, +{ + fn range(mut self, range: impl Res<(f32, f32)>) -> Self { + let e = self.entity(); + + range.set_or_bind(self.context(), e, move |cx, r| { + (*cx).emit_to(e, OscilloscopeEvents::UpdateRange(r)); + }); + + self + } + fn scaling(mut self, scaling: impl Res) -> Self { + let e = self.entity(); + + scaling.set_or_bind(self.context(), e, move |cx, s| { + (*cx).emit_to(e, OscilloscopeEvents::UpdateScaling(s)); + }); + + self + } } diff --git a/src/visualizers/unit_ruler.rs b/src/visualizers/unit_ruler.rs index 2282f19..5bfc7e7 100644 --- a/src/visualizers/unit_ruler.rs +++ b/src/visualizers/unit_ruler.rs @@ -31,23 +31,21 @@ pub struct UnitRuler {} impl UnitRuler { pub fn new<'a>( - cx: &mut Context, - display_range: impl Res<(f32, f32)>, + cx: &'a mut Context, + range: (f32, f32), scaling: ValueScaling, - values: impl Res>, + values: Vec<(f32, &'static str)>, orientation: Orientation, - ) -> Handle { + ) -> Handle<'a, Self> { Self {}.build(cx, |cx| { - let display_range = display_range.get_val(cx); let normalized_values = values - .get_val(cx) .into_iter() - .map(|v| { + .filter_map(|v| { // Normalize the value according to the provided scaling, within the provided range - ( - scaling.value_to_normalized(v.0, display_range.0, display_range.1), - v.1, - ) + scaling + .value_to_normalized_optional(v.0, range.0, range.1) + // If it is not in range, discard it by returning a `None`, which filter_map filters out + .map(|value| (value, v.1)) }) .collect::>(); ZStack::new(cx, |cx| {