1
0
Fork 0

Move analyzer drawing code to functions

This commit is contained in:
Robbert van der Helm 2023-03-20 17:34:04 +01:00
parent 6e3d2379b0
commit 29fde14c88

View file

@ -23,6 +23,11 @@ use std::sync::{Arc, Mutex};
use crate::analyzer::AnalyzerData; 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 /// A very analyzer showing the envelope followers as a magnitude spectrum with an overlay for the
/// gain reduction. /// gain reduction.
pub struct Analyzer { pub struct Analyzer {
@ -64,7 +69,17 @@ impl View for Analyzer {
return; 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() { let border_width = match cx.border_width().unwrap_or_default() {
Units::Pixels(val) => val, Units::Pixels(val) => val,
Units::Percentage(val) => bounds.w.min(bounds.h) * (val / 100.0), 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(); 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 mut path = vg::Path::new();
{ {
let x = bounds.x + border_width / 2.0; let x = bounds.x + border_width / 2.0;
@ -175,3 +104,94 @@ impl View for Analyzer {
canvas.stroke_path(&mut path, &paint); 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);
}
}
}