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 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

View file

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// 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
/// 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],

View file

@ -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

View file

@ -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;