Add automatic normalization to Buffr Glitch
This commit is contained in:
parent
7c04ec856f
commit
179ff6a035
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
use nih_plug::prelude::*;
|
use nih_plug::prelude::*;
|
||||||
|
|
||||||
|
use crate::NormalizationMode;
|
||||||
|
|
||||||
/// A super simple ring buffer abstraction that records audio into a recording ring buffer, and then
|
/// 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
|
/// 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
|
/// 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
|
/// Prepare the playback buffers to play back audio at the specified frequency. This copies
|
||||||
/// audio from the ring buffers to the playback buffers.
|
/// 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;
|
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
|
// 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]);
|
.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
|
// Reading from the buffer should always start at the beginning
|
||||||
self.playback_buffer_pos = 0;
|
self.playback_buffer_pos = 0;
|
||||||
}
|
}
|
||||||
|
@ -139,3 +157,20 @@ impl RingBuffer {
|
||||||
sample
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -33,9 +33,24 @@ struct BuffrGlitch {
|
||||||
midi_note_id: Option<u8>,
|
midi_note_id: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Normalize option
|
|
||||||
#[derive(Params)]
|
#[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 {
|
impl Default for BuffrGlitch {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -52,7 +67,9 @@ impl Default for BuffrGlitch {
|
||||||
|
|
||||||
impl Default for BuffrGlitchParams {
|
impl Default for BuffrGlitchParams {
|
||||||
fn default() -> Self {
|
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
|
// We'll copy audio to the playback buffer to match the pitch of the note
|
||||||
// that was just played
|
// 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) => {
|
NoteEvent::NoteOff { note, .. } if self.midi_note_id == Some(note) => {
|
||||||
// A NoteOff for the currently playing note immediately ends playback
|
// A NoteOff for the currently playing note immediately ends playback
|
||||||
|
|
Loading…
Reference in a new issue