1
0
Fork 0

Smooth the Diopser spectrum analyzer

This commit is contained in:
Robbert van der Helm 2022-11-18 01:31:31 +01:00
parent 1bec387216
commit 420c3099f5
2 changed files with 46 additions and 28 deletions

View file

@ -331,6 +331,10 @@ impl Plugin for Diopser {
self.sample_rate self.sample_rate
.store(buffer_config.sample_rate, Ordering::Relaxed); .store(buffer_config.sample_rate, Ordering::Relaxed);
// The spectrum is smoothed so it decays gradually
self.spectrum_input
.update_sample_rate(buffer_config.sample_rate);
true true
} }

View file

@ -26,6 +26,10 @@ pub const SPECTRUM_WINDOW_SIZE: usize = 2048;
// Don't need that much precision here // Don't need that much precision here
const SPECTRUM_WINDOW_OVERLAP: usize = 2; const SPECTRUM_WINDOW_OVERLAP: usize = 2;
/// The time it takes for the spectrum to go down 12 dB. The upwards step is immediate like in a
/// peak meter.
const SMOOTHING_DECAY_MS: f32 = 100.0;
/// The amplitudes of all frequency bins in a windowed FFT of the input. Also includes the DC offset /// The amplitudes of all frequency bins in a windowed FFT of the input. Also includes the DC offset
/// bin which we don't draw, just to make this a bit less confusing. /// bin which we don't draw, just to make this a bit less confusing.
pub type Spectrum = [f32; SPECTRUM_WINDOW_SIZE / 2 + 1]; pub type Spectrum = [f32; SPECTRUM_WINDOW_SIZE / 2 + 1];
@ -39,6 +43,11 @@ pub struct SpectrumInput {
/// The number of channels we're working on. /// The number of channels we're working on.
num_channels: usize, num_channels: usize,
/// The spectrum behaves like a peak meter. If the new value is higher than the previous one, it
/// jump up immediately. Otherwise the old value is multiplied by this weight and the new value
/// by one minus this weight.
smoothing_decay_weight: f32,
/// A way to send data to the corresponding [`SpectrumOutput`]. `spectrum_result_buffer` gets /// A way to send data to the corresponding [`SpectrumOutput`]. `spectrum_result_buffer` gets
/// copied into this buffer every time a new spectrum is available. /// copied into this buffer every time a new spectrum is available.
triple_buffer_input: triple_buffer::Input<Spectrum>, triple_buffer_input: triple_buffer::Input<Spectrum>,
@ -64,6 +73,9 @@ impl SpectrumInput {
stft: util::StftHelper::new(num_channels, SPECTRUM_WINDOW_SIZE, 0), stft: util::StftHelper::new(num_channels, SPECTRUM_WINDOW_SIZE, 0),
num_channels, num_channels,
// This is set in `initialize()` based on the sample rate
smoothing_decay_weight: 0.0,
triple_buffer_input, triple_buffer_input,
spectrum_result_buffer: [0.0; SPECTRUM_WINDOW_SIZE / 2 + 1], spectrum_result_buffer: [0.0; SPECTRUM_WINDOW_SIZE / 2 + 1],
@ -79,12 +91,25 @@ impl SpectrumInput {
(input, triple_buffer_output) (input, triple_buffer_output)
} }
/// Update the smoothing using the specified sample rate. Called in `initialize()`.
pub fn update_sample_rate(&mut self, sample_rate: f32) {
// We'll express the dacay rate in the time it takes for the moving average to drop by 12 dB
// NOTE: The effective sample rate accounts for the STFT interval, **and** for the number of
// channels. We'll average both channels to mono-ish.
let effective_sample_rate = sample_rate / SPECTRUM_WINDOW_SIZE as f32
* SPECTRUM_WINDOW_OVERLAP as f32
* self.num_channels as f32;
let decay_samples = (SMOOTHING_DECAY_MS / 1000.0 * effective_sample_rate) as f64;
self.smoothing_decay_weight = 0.25f64.powf(decay_samples.recip()) as f32
}
/// Compute the spectrum for a buffer and send it to the corresponding output pair. /// Compute the spectrum for a buffer and send it to the corresponding output pair.
pub fn compute(&mut self, buffer: &Buffer) { pub fn compute(&mut self, buffer: &Buffer) {
self.stft.process_analyze_only( self.stft.process_analyze_only(
buffer, buffer,
SPECTRUM_WINDOW_OVERLAP, SPECTRUM_WINDOW_OVERLAP,
|channel_idx, real_fft_scratch_buffer| { |_channel_idx, real_fft_scratch_buffer| {
multiply_with_window(real_fft_scratch_buffer, &self.compensated_window_function); multiply_with_window(real_fft_scratch_buffer, &self.compensated_window_function);
self.plan self.plan
@ -96,33 +121,22 @@ impl SpectrumInput {
) )
.unwrap(); .unwrap();
// To be able to reuse `real_fft_scratch_buffer` this function is called per // We'll use peak meter-like behavior for the spectrum analyzer to make things
// channel, so we need to use the channel index to do any pre- or post-processing. // easier to dial in. Values that are higher than the old value snap to the new
// Gain compensation has already been baked into the window function. // value immediately, lower values decay gradually. This also results in quasi-mono
// TODO: This obviously needs a low-pass/moving average // summing since this same callback will be called for both channels. Gain
if channel_idx == 0 { // compensation has already been baked into the window function.
for (bin, spectrum_result) in self for (bin, spectrum_result) in self
.complex_fft_buffer .complex_fft_buffer
.iter() .iter()
.zip(&mut self.spectrum_result_buffer) .zip(&mut self.spectrum_result_buffer)
{ {
*spectrum_result = bin.norm(); let magnetude = bin.norm();
} if magnetude > *spectrum_result {
} else { *spectrum_result = magnetude;
for (bin, spectrum_result) in self } else {
.complex_fft_buffer *spectrum_result = (*spectrum_result * self.smoothing_decay_weight)
.iter() + (magnetude * (1.0 - self.smoothing_decay_weight));
.skip(1)
.zip(&mut self.spectrum_result_buffer)
{
*spectrum_result += bin.norm();
}
}
let num_channels_recip = (self.num_channels as f32).recip();
if channel_idx == self.num_channels - 1 {
for bin in &mut self.spectrum_result_buffer {
*bin *= num_channels_recip;
} }
} }