From 1c8546ae138eae7db75f207eead1a7f8849a0dcc Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 21 Mar 2023 23:37:27 +0100 Subject: [PATCH] Show the target curve in Spectral Compressor --- plugins/spectral_compressor/CHANGELOG.md | 8 +- plugins/spectral_compressor/src/analyzer.rs | 9 ++- .../src/compressor_bank.rs | 34 +++++---- .../src/editor/analyzer.rs | 74 +++++++++++++++++-- 4 files changed, 98 insertions(+), 27 deletions(-) diff --git a/plugins/spectral_compressor/CHANGELOG.md b/plugins/spectral_compressor/CHANGELOG.md index 472df542..07152269 100644 --- a/plugins/spectral_compressor/CHANGELOG.md +++ b/plugins/spectral_compressor/CHANGELOG.md @@ -10,10 +10,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Added -- Added a basic analyzer that visualizes the spectral envelope followers and - gain reduction. The current version will be expanded a bit in the future to - show this information relative to the threshold target curve, with some - tooltips for more information. +- Added a basic analyzer that visualizes the target curve, the spectral envelope + followers and gain reduction. The current version will be expanded a bit in + the future to show this information relative to the threshold target curve, + with some tooltips for more information. ### Changed diff --git a/plugins/spectral_compressor/src/analyzer.rs b/plugins/spectral_compressor/src/analyzer.rs index a818cc15..47460092 100644 --- a/plugins/spectral_compressor/src/analyzer.rs +++ b/plugins/spectral_compressor/src/analyzer.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::curve::CurveParams; + /// The data stored used for the spectrum analyzer. This also contains the gain reduction and the /// threshold curve (which is dynamic in the sidechain matching mode). /// @@ -23,8 +25,12 @@ /// This pulls the data directly from the spectral compression part of Spectral Compressor, so the /// window size and overlap amounts are equal to the ones used by SC's main algorithm. If the /// current window size is 2048, then only the first `2048 / 2 + 1` elements in the arrays are used. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AnalyzerData { + /// The parameters used for the global threshold curve. This is used to draw the same curve used + /// by the compressors on the analyzer. + pub curve_params: CurveParams, + /// The number of used bins. This is part of the `AnalyzerData` since recomputing it in the /// editor could result in a race condition. pub num_bins: usize, @@ -46,6 +52,7 @@ pub struct AnalyzerData { impl Default for AnalyzerData { fn default() -> Self { Self { + curve_params: CurveParams::default(), num_bins: 0, envelope_followers: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], gain_difference_db: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], diff --git a/plugins/spectral_compressor/src/compressor_bank.rs b/plugins/spectral_compressor/src/compressor_bank.rs index 23646ff8..a1d4e897 100644 --- a/plugins/spectral_compressor/src/compressor_bank.rs +++ b/plugins/spectral_compressor/src/compressor_bank.rs @@ -286,6 +286,24 @@ impl ThresholdParams { .with_string_to_value(formatters::s2v_f32_percentage()), } } + + /// Build [`CurveParams`] out of this set of parameters. + pub fn curve_params(&self) -> CurveParams { + CurveParams { + intercept: self.threshold_db.value(), + center_frequency: self.center_frequency.value(), + // The cheeky 3 additional dB/octave attenuation is to match pink noise with the + // default settings. When using sidechaining we explicitly don't want this because + // the curve should be a flat offset to the sidechain input at the default settings. + slope: match self.mode.value() { + ThresholdMode::Internal => self.curve_slope.value() - 3.0, + ThresholdMode::SidechainMatch | ThresholdMode::SidechainCompress => { + self.curve_slope.value() + } + }, + curve: self.curve_curve.value(), + } + } } impl CompressorBankParams { @@ -602,6 +620,7 @@ impl CompressorBank { let analyzer_input_data = self.analyzer_input_data.input_buffer(); // The editor needs to know about this too so it can draw the spectra correctly + analyzer_input_data.curve_params = params.threshold.curve_params(); analyzer_input_data.num_bins = num_bins; // The gain reduction data needs to be averaged, see above @@ -984,20 +1003,7 @@ impl CompressorBank { /// are updated in accordance to the atomic flags set on this struct. fn update_if_needed(&mut self, params: &SpectralCompressorParams) { // The threshold curve is a polynomial in log-log (decibels-octaves) space - let curve_params = CurveParams { - intercept: params.threshold.threshold_db.value(), - center_frequency: params.threshold.center_frequency.value(), - // The cheeky 3 additional dB/octave attenuation is to match pink noise with the - // default settings. When using sidechaining we explicitly don't want this because - // the curve should be a flat offset to the sidechain input at the default settings. - slope: match params.threshold.mode.value() { - ThresholdMode::Internal => params.threshold.curve_slope.value() - 3.0, - ThresholdMode::SidechainMatch | ThresholdMode::SidechainCompress => { - params.threshold.curve_slope.value() - } - }, - curve: params.threshold.curve_curve.value(), - }; + let curve_params = params.threshold.curve_params(); let curve = Curve::new(&curve_params); if self diff --git a/plugins/spectral_compressor/src/editor/analyzer.rs b/plugins/spectral_compressor/src/editor/analyzer.rs index 9437b5ab..26f53725 100644 --- a/plugins/spectral_compressor/src/editor/analyzer.rs +++ b/plugins/spectral_compressor/src/editor/analyzer.rs @@ -22,11 +22,16 @@ use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use crate::analyzer::AnalyzerData; +use crate::curve::Curve; // 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; +#[allow(unused)] +const FREQ_RANGE_START_HZ: f32 = 30.0; +#[allow(unused)] +const FREQ_RANGE_END_HZ: f32 = 22_000.0; +const LN_FREQ_RANGE_START_HZ: f32 = 3.4011974; // 30.0f32.ln(); +const LN_FREQ_RANGE_END_HZ: f32 = 9.998797; // 22_000.0f32.ln(); +const LN_FREQ_RANGE: f32 = LN_FREQ_RANGE_END_HZ - LN_FREQ_RANGE_START_HZ; /// The color used for drawing the overlay. Currently not configurable using the style sheet (that /// would be possible by moving this to a dedicated view and overlaying that). @@ -37,6 +42,10 @@ const LN_FREQ_RANGE: f32 = LN_22_KHZ - LN_40_HZ; /// backgrounds. const GR_BAR_OVERLAY_COLOR: vg::Color = vg::Color::rgbaf(0.85, 0.95, 1.0, 0.8); +/// The color used for drawing the target curve. Looks somewhat similar to `GR_BAR_OVERLAY_COLOR` +/// when factoring in the blending +const TARGET_CURVE_COLOR: vg::Color = vg::Color::rgbaf(0.45, 0.55, 0.6, 0.9); + /// A very analyzer showing the envelope followers as a magnitude spectrum with an overlay for the /// gain reduction. pub struct Analyzer { @@ -84,7 +93,7 @@ impl View for Analyzer { let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; draw_spectrum(cx, canvas, analyzer_data, nyquist); - // TODO: Draw target curve + draw_target_curve(cx, canvas, analyzer_data); draw_gain_reduction(cx, canvas, analyzer_data, nyquist); // TODO: Display the frequency range below the graph @@ -114,6 +123,13 @@ impl View for Analyzer { } } +/// Compute an unclamped value based on a decibel value -80 and is mapped to 0, +20 is mapped to 1, +/// and all other values are linearly interpolated from there +#[inline] +fn db_to_unclamped_t(db_value: f32) -> f32 { + (db_value + 80.0) / 100.0 +} + /// Draw the spectrum analyzer part of the analyzer. These are drawn as vertical bars until the /// spacing between the bars becomes less the line width, at which point it's drawn as a solid mesh /// instead. @@ -139,13 +155,14 @@ fn draw_spectrum( // The frequency belonging to a bin in Hz let bin_frequency = |bin_idx: f32| (bin_idx / analyzer_data.num_bins as f32) * nyquist_hz; // A `[0, 1]` value indicating at which relative x-coordinate a bin should be drawn at - let bin_t = |bin_idx: f32| (bin_frequency(bin_idx).ln() - LN_40_HZ) / LN_FREQ_RANGE; + let bin_t = + |bin_idx: f32| (bin_frequency(bin_idx).ln() - LN_FREQ_RANGE_START_HZ) / LN_FREQ_RANGE; // Converts a linear magnitude value in to a `[0, 1]` value where 0 is -80 dB or lower, and 1 is // +20 dB or higher. let magnitude_height = |magnitude: f32| { nih_debug_assert!(magnitude >= 0.0); let magnitude_db = nih_plug::util::gain_to_db(magnitude); - ((magnitude_db + 80.0) / 100.0).clamp(0.0, 1.0) + db_to_unclamped_t(magnitude_db).clamp(0.0, 1.0) }; // The first part of this drawing routing is simple. Individual bins are drawn as bars until the @@ -240,6 +257,47 @@ fn draw_spectrum( canvas.fill_path(&mut mesh_path, &mesh_paint); } +/// Overlays the target curve over the spectrum analyzer. +fn draw_target_curve(cx: &mut DrawContext, canvas: &mut Canvas, analyzer_data: &AnalyzerData) { + let bounds = cx.bounds(); + + let line_width = cx.style.dpi_factor as f32 * 3.0; + let paint = vg::Paint::color(TARGET_CURVE_COLOR).with_line_width(line_width); + + // This can be done slightly cleverer but for our purposes drawing line segments that are either + // 1 pixel apart or that split the curve up into 100 segments (whichever results in the least + // amount of line segments) should be sufficient + let curve = Curve::new(&analyzer_data.curve_params); + let num_points = 100.min(bounds.w.ceil() as usize); + + let mut path = vg::Path::new(); + for i in 0..num_points { + let x_t = i as f32 / (num_points - 1) as f32; + let ln_freq = LN_FREQ_RANGE_START_HZ + (LN_FREQ_RANGE * x_t); + + // Evaluating the curve results in a value in dB, which must then be mapped to the same + // scale used in `draw_spectrum()` + let y_db = curve.evaluate_ln(ln_freq); + let y_t = db_to_unclamped_t(y_db); + + let physical_x_pos = bounds.x + (bounds.w * x_t); + // This value increases from bottom to top + let physical_y_pos = bounds.y + (bounds.h * (1.0 - y_t)); + + if i == 0 { + path.move_to(physical_x_pos, physical_y_pos); + } else { + path.line_to(physical_x_pos, physical_y_pos); + } + } + + // This does a way better job at cutting off the tops and bottoms of the graph than we could do + // by hand + canvas.scissor(bounds.x, bounds.y, bounds.w, bounds.h); + canvas.stroke_path(&mut path, &paint); + canvas.reset_scissor(); +} + /// Overlays the gain reduction display over the spectrum analyzer. fn draw_gain_reduction( cx: &mut DrawContext, @@ -273,13 +331,13 @@ fn draw_gain_reduction( 0.0 } else { let gr_start_ln_frequency = bin_frequency(bin_idx as f32 - 0.5).ln(); - (gr_start_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE + (gr_start_ln_frequency - LN_FREQ_RANGE_START_HZ) / LN_FREQ_RANGE }; let t_end = if bin_idx == analyzer_data.num_bins - 1 { 1.0 } else { let gr_end_ln_frequency = bin_frequency(bin_idx as f32 + 0.5).ln(); - (gr_end_ln_frequency - LN_40_HZ) / LN_FREQ_RANGE + (gr_end_ln_frequency - LN_FREQ_RANGE_START_HZ) / LN_FREQ_RANGE }; if t_end < 0.0 || t_start > 1.0 { continue;