From 29fde14c881e331b9f294de3ca5de26f4b4ffd22 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 20 Mar 2023 17:34:04 +0100 Subject: [PATCH] Move analyzer drawing code to functions --- .../src/editor/analyzer.rs | 194 ++++++++++-------- 1 file changed, 107 insertions(+), 87 deletions(-) diff --git a/plugins/spectral_compressor/src/editor/analyzer.rs b/plugins/spectral_compressor/src/editor/analyzer.rs index 72de9c4b..6c2630b8 100644 --- a/plugins/spectral_compressor/src/editor/analyzer.rs +++ b/plugins/spectral_compressor/src/editor/analyzer.rs @@ -23,6 +23,11 @@ use std::sync::{Arc, Mutex}; use crate::analyzer::AnalyzerData; +// We'll show the bins from 30 Hz (to your chest) to 22 kHz, scaled logarithmically +const LN_40_HZ: f32 = 3.4011974; // 30.0f32.ln(); +const LN_22_KHZ: f32 = 9.998797; // 22000.0f32.ln(); +const LN_FREQ_RANGE: f32 = LN_22_KHZ - LN_40_HZ; + /// A very analyzer showing the envelope followers as a magnitude spectrum with an overlay for the /// gain reduction. pub struct Analyzer { @@ -64,7 +69,17 @@ impl View for Analyzer { return; } - // This only covers the style rules we're actually using + // The analyzer data is pulled directly from the spectral `CompressorBank` + let mut analyzer_data = self.analyzer_data.lock().unwrap(); + let analyzer_data = analyzer_data.read(); + let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; + + draw_spectrum(cx, canvas, analyzer_data, nyquist); + // TODO: Draw target curve + draw_gain_reduction(cx, canvas, analyzer_data, nyquist); + // TODO: Display the frequency range below the graph + + // Draw the border last let border_width = match cx.border_width().unwrap_or_default() { Units::Pixels(val) => val, Units::Percentage(val) => bounds.w.min(bounds.h) * (val / 100.0), @@ -72,92 +87,6 @@ impl View for Analyzer { }; let border_color: vg::Color = cx.border_color().cloned().unwrap_or_default().into(); - // Used for the spectrum analyzer lines - let line_width = cx.style.dpi_factor as f32 * 1.5; - let text_color: vg::Color = cx.font_color().cloned().unwrap_or_default().into(); - let spectrum_paint = vg::Paint::color(text_color).with_line_width(line_width); - // Used for the gain reduction bars. Lighter and semitransparent to make it stand out - // against the spectrum analyzer - let bar_paint_color = vg::Color::rgbaf(0.7, 0.9, 1.0, 0.7); - let bar_paint = vg::Paint::color(bar_paint_color); - - // The analyzer data is pulled directly from the spectral `CompressorBank` - let mut analyzer_data = self.analyzer_data.lock().unwrap(); - let analyzer_data = analyzer_data.read(); - let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; - let bin_frequency = |bin_idx: f32| (bin_idx / analyzer_data.num_bins as f32) * nyquist; - - // TODO: Draw individual bars until the difference between the next two bars becomes less - // than one pixel. At that point draw it as a single mesh to get rid of aliasing. - for (bin_idx, (magnetude, gain_difference_db)) in analyzer_data - .envelope_followers - .iter() - .zip(analyzer_data.gain_difference_db.iter()) - .enumerate() - { - // We'll show the bins from 30 Hz (to your chest) to 22 kHz, scaled logarithmically - const LN_40_HZ: f32 = 3.4011974; // 30.0f32.ln(); - const LN_22_KHZ: f32 = 9.998797; // 22000.0f32.ln(); - const LN_FREQ_RANGE: f32 = LN_22_KHZ - LN_40_HZ; - - { - let ln_frequency = bin_frequency(bin_idx as f32).ln(); - let t = (ln_frequency - LN_40_HZ) / LN_FREQ_RANGE; - if t <= 0.0 || t >= 1.0 { - 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. This is the same scaling used in - // Diopser's spectrum analyzer. - 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, &spectrum_paint); - } - - // TODO: Visualize the target curve - - // TODO: Draw this as a single mesh instead, this doesn't work. - // Avoid drawing tiny slivers for low gain reduction values - if gain_difference_db.abs() > 0.2 { - // The gain reduction bars are drawn width the width of the bin, centered on the - // bin's center frequency - let gr_start_ln_frequency = bin_frequency(bin_idx as f32 - 0.5).ln(); - let gr_end_ln_frequency = bin_frequency(bin_idx as f32 + 0.5).ln(); - - let t_start = ((gr_start_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE).max(0.0); - let t_end = ((gr_end_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE).min(1.0); - - // For the bar's height we'll draw 0 dB of gain reduction as a flat line (except we - // don't actually draw 0 dBs of GR because it looks glitchy, but that's besides the - // point). 40 dB of gain reduction causes the bar to be drawn from the center all - // the way to the bottom of the spectrum analyzer. 40 dB of additional gain causes - // the bar to be drawn from the center all the way to the top of the graph. - // NOTE: Y-coordinates go from top to bottom, hence the minus - // TODO: The y-position should be relative to the target curve - let t_y = ((-gain_difference_db + 40.0) / 80.0).clamp(0.0, 1.0); - - let mut path = vg::Path::new(); - path.move_to(bounds.x + (bounds.w * t_start), bounds.y + (bounds.h * 0.5)); - path.line_to(bounds.x + (bounds.w * t_end), bounds.y + (bounds.h * 0.5)); - path.line_to(bounds.x + (bounds.w * t_end), bounds.y + (bounds.h * t_y)); - path.line_to(bounds.x + (bounds.w * t_start), bounds.y + (bounds.h * t_y)); - path.close(); - canvas.fill_path(&mut path, &bar_paint); - } - } - - // TODO: Display the frequency range below the graph - - // Draw the border last let mut path = vg::Path::new(); { let x = bounds.x + border_width / 2.0; @@ -175,3 +104,94 @@ impl View for Analyzer { canvas.stroke_path(&mut path, &paint); } } + +/// Draw the spectrum analyzer part of the analyzer. These are drawn as vertical bars until the +/// spacing between the bars becomes less than 1 pixel, at which point it's drawn as a solid mesh +/// instead. +fn draw_spectrum( + cx: &mut DrawContext, + canvas: &mut Canvas, + analyzer_data: &AnalyzerData, + nyquist_hz: f32, +) { + let bounds = cx.bounds(); + + let line_width = cx.style.dpi_factor as f32 * 1.5; + let text_color: vg::Color = cx.font_color().cloned().unwrap_or_default().into(); + let spectrum_paint = vg::Paint::color(text_color).with_line_width(line_width); + + let bin_frequency = |bin_idx: f32| (bin_idx / analyzer_data.num_bins as f32) * nyquist_hz; + + // TODO: Draw individual bars until the difference between the next two bars becomes less + // than one pixel. At that point draw it as a single mesh to get rid of aliasing. + for (bin_idx, magnetude) in analyzer_data.envelope_followers.iter().enumerate() { + let ln_frequency = bin_frequency(bin_idx as f32).ln(); + let t = (ln_frequency - LN_40_HZ) / LN_FREQ_RANGE; + if t <= 0.0 || t >= 1.0 { + 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. This is the same scaling used in + // Diopser's spectrum analyzer. + 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, &spectrum_paint); + } +} + +/// Overlays the gain reduction display over the spectrum analyzer. +fn draw_gain_reduction( + cx: &mut DrawContext, + canvas: &mut Canvas, + analyzer_data: &AnalyzerData, + nyquist_hz: f32, +) { + let bounds = cx.bounds(); + + // TODO: This color should be defined elsewhere + let bar_paint_color = vg::Color::rgbaf(0.7, 0.9, 1.0, 0.7); + let bar_paint = vg::Paint::color(bar_paint_color); + + let bin_frequency = |bin_idx: f32| (bin_idx / analyzer_data.num_bins as f32) * nyquist_hz; + + // TODO: This should be drawn as one mesh, or multiple meshes if there are empty gain reduction bars + for (bin_idx, gain_difference_db) in analyzer_data.gain_difference_db.iter().enumerate() { + // TODO: Draw this as a single mesh instead, this doesn't work. + // Avoid drawing tiny slivers for low gain reduction values + if gain_difference_db.abs() > 0.2 { + // The gain reduction bars are drawn width the width of the bin, centered on the + // bin's center frequency + let gr_start_ln_frequency = bin_frequency(bin_idx as f32 - 0.5).ln(); + let gr_end_ln_frequency = bin_frequency(bin_idx as f32 + 0.5).ln(); + + let t_start = ((gr_start_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE).max(0.0); + let t_end = ((gr_end_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE).min(1.0); + + // For the bar's height we'll draw 0 dB of gain reduction as a flat line (except we + // don't actually draw 0 dBs of GR because it looks glitchy, but that's besides the + // point). 40 dB of gain reduction causes the bar to be drawn from the center all + // the way to the bottom of the spectrum analyzer. 40 dB of additional gain causes + // the bar to be drawn from the center all the way to the top of the graph. + // NOTE: Y-coordinates go from top to bottom, hence the minus + // TODO: The y-position should be relative to the target curve + let t_y = ((-gain_difference_db + 40.0) / 80.0).clamp(0.0, 1.0); + + let mut path = vg::Path::new(); + path.move_to(bounds.x + (bounds.w * t_start), bounds.y + (bounds.h * 0.5)); + path.line_to(bounds.x + (bounds.w * t_end), bounds.y + (bounds.h * 0.5)); + path.line_to(bounds.x + (bounds.w * t_end), bounds.y + (bounds.h * t_y)); + path.line_to(bounds.x + (bounds.w * t_start), bounds.y + (bounds.h * t_y)); + path.close(); + canvas.fill_path(&mut path, &bar_paint); + } + } +}