diff --git a/plugins/diopser/src/editor.rs b/plugins/diopser/src/editor.rs index a99792f1..ff15f9e8 100644 --- a/plugins/diopser/src/editor.rs +++ b/plugins/diopser/src/editor.rs @@ -27,6 +27,7 @@ use self::button::SafeModeButton; use crate::spectrum::SpectrumOutput; use crate::{Diopser, DiopserParams}; +mod analyzer; mod button; mod xy_pad; @@ -137,7 +138,9 @@ fn spectrum_analyzer(cx: &mut Context) { VStack::new(cx, |cx| { ZStack::new(cx, |cx| { - Label::new(cx, "When I grow up, I want to be a spectrum analyzer!"); + analyzer::SpectrumAnalyzer::new(cx, Data::spectrum, Data::sample_rate) + .width(Percentage(100.0)) + .height(Percentage(100.0)); xy_pad::XyPad::new( cx, @@ -148,7 +151,6 @@ fn spectrum_analyzer(cx: &mut Context) { .width(Percentage(100.0)) .height(Percentage(100.0)); }) - .child_space(Stretch(1.0)) .width(Percentage(100.0)) .background_color(DARK_GRAY) .height(Pixels(SPECTRUM_ANALYZER_HEIGHT)); diff --git a/plugins/diopser/src/editor/analyzer.rs b/plugins/diopser/src/editor/analyzer.rs new file mode 100644 index 00000000..e654fcdf --- /dev/null +++ b/plugins/diopser/src/editor/analyzer.rs @@ -0,0 +1,109 @@ +// Diopser: a phase rotation plugin +// Copyright (C) 2021-2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use atomic_float::AtomicF32; +use nih_plug::nih_debug_assert; +use nih_plug::prelude::FloatRange; +use nih_plug_vizia::vizia::prelude::*; +use nih_plug_vizia::vizia::vg; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; + +use crate::spectrum::SpectrumOutput; + +/// A very abstract spectrum analyzer. This draws the magnitude spectrum's bins as vertical lines +/// with the same distirubtion as the filter frequency parmaeter.. +pub struct SpectrumAnalyzer { + spectrum: Arc>, + sample_rate: Arc, + + /// The same range as that used by the filter frequency parameter. We'll use this to make sure + /// we draw the spectrum analyzer's ticks at locations that match the frequency parameter linked + /// to the X-Y pad's X-axis. + frequency_range: FloatRange, +} + +impl SpectrumAnalyzer { + /// Creates a new [`SpectrumAnalyzer`]. The uses custom drawing. + pub fn new( + cx: &mut Context, + spectrum: LSpectrum, + sample_rate: LRate, + ) -> Handle + where + LSpectrum: Lens>>, + LRate: Lens>, + { + Self { + spectrum: spectrum.get(cx), + sample_rate: sample_rate.get(cx), + + frequency_range: crate::filter_frequency_range(), + } + .build( + cx, + // This is an otherwise empty element only used for custom drawing + |_cx| (), + ) + } +} + +impl View for SpectrumAnalyzer { + fn element(&self) -> Option<&'static str> { + Some("spectrum-analyzer") + } + + fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + let bounds = cx.bounds(); + if bounds.w == 0.0 || bounds.h == 0.0 { + return; + } + + // This spectrum buffer is written to at the end of the process function when the editor is + // open + let mut spectrum = self.spectrum.lock().unwrap(); + let spectrum = spectrum.read(); + let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; + + // This skips background and border drawing + let line_width = cx.style.dpi_factor as f32 * 1.5; + let paint = vg::Paint::color(cx.font_color().cloned().unwrap_or_default().into()) + .with_line_width(line_width); + for (bin_idx, magnetude) in spectrum.iter().enumerate() { + // We'll match up the bin's x-coordinate with the filter frequency parameter + let frequency = (bin_idx as f32 / spectrum.len() as f32) * nyquist; + let t = self.frequency_range.normalize(frequency); + if !(0.0..=1.0).contains(&t) { + continue; + } + + // Scale this so that 1.0/0 dBFS magnetude is at 80% of the height, the bars begin at + // -80 dBFS, and that the scaling is linear + nih_debug_assert!(*magnetude >= 0.0); + let magnetude_db = nih_plug::util::gain_to_db(*magnetude); + let height = ((magnetude_db + 80.0) / 100.0).clamp(0.0, 1.0); + + let mut path = vg::Path::new(); + path.move_to( + bounds.x + (bounds.w * t), + bounds.y + (bounds.h * (1.0 - height)), + ); + path.line_to(bounds.x + (bounds.w * t), bounds.y + bounds.h); + + canvas.stroke_path(&mut path, &paint); + } + } +} diff --git a/plugins/diopser/src/editor/theme.css b/plugins/diopser/src/editor/theme.css index 437d9bff..7ed44f53 100644 --- a/plugins/diopser/src/editor/theme.css +++ b/plugins/diopser/src/editor/theme.css @@ -32,6 +32,10 @@ param-button.safe-mode:checked { width: 170px; } +spectrum-analyzer { + color: #0a0a0a; +} + xy-pad { overflow: hidden; }