1
0
Fork 0

Add automatic normalization to Buffr Glitch

This commit is contained in:
Robbert van der Helm 2022-11-09 17:44:15 +01:00
parent 7c04ec856f
commit 179ff6a035
2 changed files with 60 additions and 5 deletions

View file

@ -16,6 +16,8 @@
use nih_plug::prelude::*;
use crate::NormalizationMode;
/// A super simple ring buffer abstraction that records audio into a recording ring buffer, and then
/// copies audio to a playback buffer when a note is pressed so audio can be repeated while still
/// recording audio for further key presses. This needs to be able to store at least the number of
@ -94,7 +96,7 @@ impl RingBuffer {
/// Prepare the playback buffers to play back audio at the specified frequency. This copies
/// audio from the ring buffers to the playback buffers.
pub fn prepare_playback(&mut self, frequency: f32) {
pub fn prepare_playback(&mut self, frequency: f32, normalization_mode: NormalizationMode) {
let note_period_samples = (frequency.recip() * self.sample_rate).ceil() as usize;
// We'll copy the last `note_period_samples` samples from the recording ring buffers to the
@ -117,6 +119,22 @@ impl RingBuffer {
.copy_from_slice(&recording_buffer[..copy_num_from_start]);
}
// The playback buffer is normalized as necessary. This prevents small grains from being
// either way quieter or way louder than the origianl audio.
match normalization_mode {
NormalizationMode::None => (),
NormalizationMode::Auto => {
let normalization_factor =
calculate_rms(&self.recording_buffers) / calculate_rms(&self.playback_buffers);
for buffer in self.playback_buffers.iter_mut() {
for sample in buffer.iter_mut() {
*sample *= normalization_factor;
}
}
}
}
// Reading from the buffer should always start at the beginning
self.playback_buffer_pos = 0;
}
@ -139,3 +157,20 @@ impl RingBuffer {
sample
}
}
/// Get the RMS value of an entire buffer. This is used for (automatic) normalization.
///
/// # Panics
///
/// This will panic of `buffers` is empty.
fn calculate_rms(buffers: &[Vec<f32>]) -> f32 {
nih_debug_assert_ne!(buffers.len(), 0);
let sum_of_squares: f32 = buffers
.iter()
.map(|buffer| buffer.iter().map(|sample| (sample * sample)).sum::<f32>())
.sum();
let num_samples = buffers.len() * buffers[0].len();
(sum_of_squares / num_samples as f32).sqrt()
}

View file

@ -33,9 +33,24 @@ struct BuffrGlitch {
midi_note_id: Option<u8>,
}
// TODO: Normalize option
#[derive(Params)]
struct BuffrGlitchParams {}
struct BuffrGlitchParams {
/// Controls if and how grains are normalization.
#[id = "normalization_mode"]
normalization_mode: EnumParam<NormalizationMode>,
}
/// Controls how grains are normalized.
#[derive(Enum, Debug, PartialEq, Eq)]
pub enum NormalizationMode {
/// Don't normalize at all
#[id = "none"]
None,
/// Automatically normalize based on the recording buffer's RMS value.
#[id = "auto"]
Auto,
// TODO: Explicit RMS target
}
impl Default for BuffrGlitch {
fn default() -> Self {
@ -52,7 +67,9 @@ impl Default for BuffrGlitch {
impl Default for BuffrGlitchParams {
fn default() -> Self {
Self {}
Self {
normalization_mode: EnumParam::new("Normalization", NormalizationMode::Auto),
}
}
}
@ -124,7 +141,10 @@ impl Plugin for BuffrGlitch {
// We'll copy audio to the playback buffer to match the pitch of the note
// that was just played
self.buffer.prepare_playback(util::midi_note_to_freq(note));
self.buffer.prepare_playback(
util::midi_note_to_freq(note),
self.params.normalization_mode.value(),
);
}
NoteEvent::NoteOff { note, .. } if self.midi_note_id == Some(note) => {
// A NoteOff for the currently playing note immediately ends playback