diff --git a/plugins/spectral_compressor/src/analyzer.rs b/plugins/spectral_compressor/src/analyzer.rs index ae60ec94..a818cc15 100644 --- a/plugins/spectral_compressor/src/analyzer.rs +++ b/plugins/spectral_compressor/src/analyzer.rs @@ -34,10 +34,10 @@ pub struct AnalyzerData { /// This data is taken directly from the envelope followers, so it has the same rise and fall /// time as what is used by the compressors. pub envelope_followers: [f32; crate::MAX_WINDOW_SIZE / 2 + 1], - /// The gain reduction applied to each band, in decibels. Positive values mean that a band - /// becomes louder, and negative values mean a band got attenuated. Does not (and should not) - /// factor in the output gain. - pub gain_reduction_db: [f32; crate::MAX_WINDOW_SIZE / 2 + 1], + /// The gain different applied to each band, in decibels. Alternatively, the negative gain + /// reduction. Positive values mean that a band becomes louder, and negative values mean a band + /// got attenuated. Does not (and should not) factor in the output gain. + pub gain_difference_db: [f32; crate::MAX_WINDOW_SIZE / 2 + 1], // TODO: Include the threshold curve. Decide on whether to only visualizer the 'global' // threshold curve or to also show the individual upwards/downwards thresholds. Or omit // this and implement it in a nicer way for the premium Spectral Compressor. @@ -48,7 +48,7 @@ impl Default for AnalyzerData { Self { num_bins: 0, envelope_followers: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], - gain_reduction_db: [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 47391b57..e3209b96 100644 --- a/plugins/spectral_compressor/src/compressor_bank.rs +++ b/plugins/spectral_compressor/src/compressor_bank.rs @@ -560,9 +560,9 @@ impl CompressorBank { ) { nih_debug_assert_eq!(buffer.len(), self.log2_freqs.len()); - // The gain reduction amounts are accumulated in `self.analyzer_input_data`. When processing - // the last channel, this data is divided by the channel count, the envelope follower data - // is added, and the data is then sent to the editor so it can be displayed. + // The gain difference/reduction amounts are accumulated in `self.analyzer_input_data`. When + // processing the last channel, this data is divided by the channel count, the envelope + // follower data is added, and the data is then sent to the editor so it can be displayed. // `analyzer_input_data` contains excess capacity so it can handle any supported window // size, so all operations on it are limited to the actual number of used bins. let num_bins = buffer.len(); @@ -573,7 +573,7 @@ impl CompressorBank { // just been opened. If this doesn't look too obvious or too jarring this is // probably worth letting it be like this. let analyzer_input_data = self.analyzer_input_data.input_buffer(); - analyzer_input_data.gain_reduction_db[..num_bins].fill(0.0); + analyzer_input_data.gain_difference_db[..num_bins].fill(0.0); } self.update_if_needed(params); @@ -605,8 +605,8 @@ impl CompressorBank { // The gain reduction data needs to be averaged, see above let channel_multiplier = (num_channels as f32).recip(); - for gain_reduction_db in &mut analyzer_input_data.gain_reduction_db[..num_bins] { - *gain_reduction_db *= channel_multiplier; + for gain_difference_db in &mut analyzer_input_data.gain_difference_db[..num_bins] { + *gain_difference_db *= channel_multiplier; } // The spectrum analyzer data has not yet been added @@ -784,7 +784,7 @@ impl CompressorBank { let downwards_knee_width_db = params.compressors.downwards.knee_width_db.value(); let upwards_knee_width_db = params.compressors.upwards.knee_width_db.value(); - assert!(analyzer_input_data.gain_reduction_db.len() >= buffer.len()); + assert!(analyzer_input_data.gain_difference_db.len() >= buffer.len()); assert!(self.downwards_thresholds_db.len() == buffer.len()); assert!(self.downwards_ratios.len() == buffer.len()); assert!(self.downwards_knee_parabola_scale.len() == buffer.len()); @@ -849,14 +849,15 @@ impl CompressorBank { // If the comprssed output is -10 dBFS and the envelope follower was at -6 dBFS, then we // want to apply -4 dB of gain to the bin - let gain_reduction_db = downwards_compressed + upwards_compressed - (envelope_db * 2.0); + let gain_difference_db = + downwards_compressed + upwards_compressed - (envelope_db * 2.0); unsafe { *analyzer_input_data - .gain_reduction_db - .get_unchecked_mut(bin_idx) += gain_reduction_db; + .gain_difference_db + .get_unchecked_mut(bin_idx) += gain_difference_db; } - *bin *= util::db_to_gain_fast(gain_reduction_db); + *bin *= util::db_to_gain_fast(gain_difference_db); } } @@ -885,7 +886,7 @@ impl CompressorBank { let other_channels_t = params.threshold.sc_channel_link.value() / num_channels; let this_channel_t = 1.0 - (other_channels_t * (num_channels - 1.0)); - assert!(analyzer_input_data.gain_reduction_db.len() >= buffer.len()); + assert!(analyzer_input_data.gain_difference_db.len() >= buffer.len()); assert!(self.sidechain_spectrum_magnitudes[channel_idx].len() == buffer.len()); assert!(self.downwards_thresholds_db.len() == buffer.len()); assert!(self.downwards_ratios.len() == buffer.len()); @@ -966,14 +967,15 @@ impl CompressorBank { // If the comprssed output is -10 dBFS and the envelope follower was at -6 dBFS, then we // want to apply -4 dB of gain to the bin - let gain_reduction_db = downwards_compressed + upwards_compressed - (envelope_db * 2.0); + let gain_difference_db = + downwards_compressed + upwards_compressed - (envelope_db * 2.0); unsafe { *analyzer_input_data - .gain_reduction_db - .get_unchecked_mut(bin_idx) += gain_reduction_db; + .gain_difference_db + .get_unchecked_mut(bin_idx) += gain_difference_db; } - *bin *= util::db_to_gain_fast(gain_reduction_db); + *bin *= util::db_to_gain_fast(gain_difference_db); } } diff --git a/plugins/spectral_compressor/src/editor/analyzer.rs b/plugins/spectral_compressor/src/editor/analyzer.rs index 532c8d36..72de9c4b 100644 --- a/plugins/spectral_compressor/src/editor/analyzer.rs +++ b/plugins/spectral_compressor/src/editor/analyzer.rs @@ -64,29 +64,35 @@ impl View for Analyzer { return; } - // This only covers the style rules we're actually setting. Right now this doesn't support - // backgrounds. - let opacity = cx.opacity(); + // This only covers the style rules we're actually using 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), _ => 0.0, }; - let mut border_color: vg::Color = cx.border_color().cloned().unwrap_or_default().into(); - border_color.set_alphaf(border_color.a * opacity); + 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; - let line_width = cx.style.dpi_factor as f32 * 1.5; - let paint = vg::Paint::color(cx.font_color().cloned().unwrap_or_default().into()) - .with_line_width(line_width); - for (bin_idx, (magnetude, gain_reduction_db)) in analyzer_data + // 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_reduction_db.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 @@ -94,30 +100,59 @@ impl View for Analyzer { const LN_22_KHZ: f32 = 9.998797; // 22000.0f32.ln(); const LN_FREQ_RANGE: f32 = LN_22_KHZ - LN_40_HZ; - let frequency = (bin_idx as f32 / analyzer_data.num_bins as f32) * nyquist; - let ln_frequency = frequency.ln(); - let t = (ln_frequency - LN_40_HZ) / LN_FREQ_RANGE; - if t <= 0.0 || t >= 1.0 { - continue; + { + 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); } - // 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, &paint); - - // TODO: Visualize the gain reduction // 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 @@ -133,7 +168,6 @@ impl View for Analyzer { path.line_to(x, y + h); path.line_to(x + w, y + h); path.line_to(x + w, y); - path.line_to(x, y); path.close(); }