1
0
Fork 0

Show the target curve in Spectral Compressor

This commit is contained in:
Robbert van der Helm 2023-03-21 23:37:27 +01:00
parent 144fafbed6
commit 1c8546ae13
4 changed files with 98 additions and 27 deletions

View file

@ -10,10 +10,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Added ### Added
- Added a basic analyzer that visualizes the spectral envelope followers and - Added a basic analyzer that visualizes the target curve, the spectral envelope
gain reduction. The current version will be expanded a bit in the future to followers and gain reduction. The current version will be expanded a bit in
show this information relative to the threshold target curve, with some the future to show this information relative to the threshold target curve,
tooltips for more information. with some tooltips for more information.
### Changed ### Changed

View file

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::curve::CurveParams;
/// The data stored used for the spectrum analyzer. This also contains the gain reduction and the /// 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). /// 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 /// 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 /// 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. /// 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 { 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 /// The number of used bins. This is part of the `AnalyzerData` since recomputing it in the
/// editor could result in a race condition. /// editor could result in a race condition.
pub num_bins: usize, pub num_bins: usize,
@ -46,6 +52,7 @@ pub struct AnalyzerData {
impl Default for AnalyzerData { impl Default for AnalyzerData {
fn default() -> Self { fn default() -> Self {
Self { Self {
curve_params: CurveParams::default(),
num_bins: 0, num_bins: 0,
envelope_followers: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], envelope_followers: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1],
gain_difference_db: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], gain_difference_db: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1],

View file

@ -286,6 +286,24 @@ impl ThresholdParams {
.with_string_to_value(formatters::s2v_f32_percentage()), .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 { impl CompressorBankParams {
@ -602,6 +620,7 @@ impl CompressorBank {
let analyzer_input_data = self.analyzer_input_data.input_buffer(); 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 // 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; analyzer_input_data.num_bins = num_bins;
// The gain reduction data needs to be averaged, see above // 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. /// are updated in accordance to the atomic flags set on this struct.
fn update_if_needed(&mut self, params: &SpectralCompressorParams) { fn update_if_needed(&mut self, params: &SpectralCompressorParams) {
// The threshold curve is a polynomial in log-log (decibels-octaves) space // The threshold curve is a polynomial in log-log (decibels-octaves) space
let curve_params = CurveParams { let curve_params = params.threshold.curve_params();
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 = Curve::new(&curve_params); let curve = Curve::new(&curve_params);
if self if self

View file

@ -22,11 +22,16 @@ use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::analyzer::AnalyzerData; use crate::analyzer::AnalyzerData;
use crate::curve::Curve;
// We'll show the bins from 30 Hz (to your chest) to 22 kHz, scaled logarithmically // 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(); #[allow(unused)]
const LN_22_KHZ: f32 = 9.998797; // 22000.0f32.ln(); const FREQ_RANGE_START_HZ: f32 = 30.0;
const LN_FREQ_RANGE: f32 = LN_22_KHZ - LN_40_HZ; #[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 /// 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). /// 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. /// backgrounds.
const GR_BAR_OVERLAY_COLOR: vg::Color = vg::Color::rgbaf(0.85, 0.95, 1.0, 0.8); 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 /// 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 {
@ -84,7 +93,7 @@ impl View for Analyzer {
let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0;
draw_spectrum(cx, canvas, analyzer_data, nyquist); 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); draw_gain_reduction(cx, canvas, analyzer_data, nyquist);
// TODO: Display the frequency range below the graph // 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 /// 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 /// spacing between the bars becomes less the line width, at which point it's drawn as a solid mesh
/// instead. /// instead.
@ -139,13 +155,14 @@ fn draw_spectrum(
// The frequency belonging to a bin in Hz // The frequency belonging to a bin in Hz
let bin_frequency = |bin_idx: f32| (bin_idx / analyzer_data.num_bins as f32) * nyquist_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 // 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 // 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. // +20 dB or higher.
let magnitude_height = |magnitude: f32| { let magnitude_height = |magnitude: f32| {
nih_debug_assert!(magnitude >= 0.0); nih_debug_assert!(magnitude >= 0.0);
let magnitude_db = nih_plug::util::gain_to_db(magnitude); 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 // 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); 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. /// Overlays the gain reduction display over the spectrum analyzer.
fn draw_gain_reduction( fn draw_gain_reduction(
cx: &mut DrawContext, cx: &mut DrawContext,
@ -273,13 +331,13 @@ fn draw_gain_reduction(
0.0 0.0
} else { } else {
let gr_start_ln_frequency = bin_frequency(bin_idx as f32 - 0.5).ln(); 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 { let t_end = if bin_idx == analyzer_data.num_bins - 1 {
1.0 1.0
} else { } else {
let gr_end_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();
(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 { if t_end < 0.0 || t_start > 1.0 {
continue; continue;