Change Buffr Gltich to start recording on key down
From a 'buffer glitch' point of view the old behavior made a lot of sense, but it wasn't as musical.
This commit is contained in:
parent
886f3a78ef
commit
2a1201580c
23
plugins/buffr_glitch/CHANGELOG.md
Normal file
23
plugins/buffr_glitch/CHANGELOG.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic
|
||||||
|
Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The normalization option has temporarily been removed since the old method to
|
||||||
|
automatically normalize the buffer doesn't work anymore with below recording
|
||||||
|
change.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Buffr Glitch now starts recording when a note is held down instead of playing
|
||||||
|
back previously played audio. This makes it possible to use Buffr Glitch in a
|
||||||
|
more rhythmic way without manually offsetting notes. This is particularly
|
||||||
|
important at the start of the playback since then the buffer will have
|
||||||
|
otherwise been completely silent.
|
|
@ -18,27 +18,25 @@ use nih_plug::prelude::*;
|
||||||
|
|
||||||
use crate::{NormalizationMode, MAX_OCTAVE_SHIFT};
|
use crate::{NormalizationMode, MAX_OCTAVE_SHIFT};
|
||||||
|
|
||||||
/// 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 buffer until it is full, and
|
||||||
/// copies audio to a playback buffer when a note is pressed so audio can be repeated while still
|
/// then starts looping the already recorded audio. The recording starts hwne pressing a key so
|
||||||
/// recording audio for further key presses. This needs to be able to store at least the number of
|
/// transients are preserved correctly. This needs to be able to store at least the number of
|
||||||
/// samples that correspond to the period size of MIDI note 0.
|
/// samples that correspond to the period size of MIDI note 0.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RingBuffer {
|
pub struct RingBuffer {
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
|
|
||||||
/// Sample ring buffers indexed by channel and sample index. These are always recorded to.
|
/// When a key is pressed, `next_sample_pos` is set to 0 and the incoming audio is recorded into
|
||||||
recording_buffers: Vec<Vec<f32>>,
|
/// this buffer until `next_sample_pos` wraps back around to the start of the ring buffer. At
|
||||||
/// The positions within the sample buffers the next sample should be written to. Since all
|
/// that point the incoming audio is replaced by the previously recorded audio. These buffers
|
||||||
/// channels will be written to in lockstep we only need a single value here. It's incremented
|
/// are resized to match the length/frequency of the audio being played back.
|
||||||
/// when writing a sample for the last channel.
|
audio_buffers: Vec<Vec<f32>>,
|
||||||
next_write_pos: usize,
|
|
||||||
|
|
||||||
/// When a key is pressed, audio gets copied from `recording_buffers` to these buffers so it can
|
|
||||||
/// be played back without interrupting the recording process. These buffers are resized to
|
|
||||||
/// match the length of the audio being played back.
|
|
||||||
playback_buffers: Vec<Vec<f32>>,
|
|
||||||
/// The current playback position in `playback_buffers`.
|
/// The current playback position in `playback_buffers`.
|
||||||
playback_buffer_pos: usize,
|
next_sample_pos: usize,
|
||||||
|
/// If this is set to `false` then the incoming audio will be recorded to `playback_buffer`
|
||||||
|
/// until it is full. When it wraps around this is set to `true` and the previously recorded
|
||||||
|
/// audio is played back instead.
|
||||||
|
playback_buffer_ready: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RingBuffer {
|
impl RingBuffer {
|
||||||
|
@ -47,6 +45,9 @@ impl RingBuffer {
|
||||||
/// MIDI note 0 at the specified sample rate, rounded up to a power of two. Make sure to call
|
/// MIDI note 0 at the specified sample rate, rounded up to a power of two. Make sure to call
|
||||||
/// [`reset()`][Self::reset()] after this.
|
/// [`reset()`][Self::reset()] after this.
|
||||||
pub fn resize(&mut self, num_channels: usize, sample_rate: f32) {
|
pub fn resize(&mut self, num_channels: usize, sample_rate: f32) {
|
||||||
|
nih_debug_assert!(num_channels >= 1);
|
||||||
|
nih_debug_assert!(sample_rate > 0.0);
|
||||||
|
|
||||||
// NOTE: We need to take the octave shift into account
|
// NOTE: We need to take the octave shift into account
|
||||||
let lowest_note_frequency =
|
let lowest_note_frequency =
|
||||||
util::midi_note_to_freq(0) / 2.0f32.powi(MAX_OCTAVE_SHIFT as i32);
|
util::midi_note_to_freq(0) / 2.0f32.powi(MAX_OCTAVE_SHIFT as i32);
|
||||||
|
@ -57,129 +58,89 @@ impl RingBuffer {
|
||||||
// Used later to compute period sizes in samples based on frequencies
|
// Used later to compute period sizes in samples based on frequencies
|
||||||
self.sample_rate = sample_rate;
|
self.sample_rate = sample_rate;
|
||||||
|
|
||||||
self.recording_buffers.resize_with(num_channels, Vec::new);
|
self.audio_buffers.resize_with(num_channels, Vec::new);
|
||||||
for buffer in self.recording_buffers.iter_mut() {
|
for buffer in self.audio_buffers.iter_mut() {
|
||||||
buffer.resize(buffer_len, 0.0);
|
buffer.resize(buffer_len, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.playback_buffers.resize_with(num_channels, Vec::new);
|
|
||||||
for buffer in self.playback_buffers.iter_mut() {
|
|
||||||
buffer.resize(buffer_len, 0.0);
|
|
||||||
// We need to reserve capacity for the playback buffers, but they're initially empty
|
|
||||||
buffer.resize(0, 0.0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zero out the buffers.
|
/// Zero out the buffers.
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
for buffer in self.recording_buffers.iter_mut() {
|
// The current verion's buffers don't need to be reset since they're always initialized
|
||||||
buffer.fill(0.0);
|
// before being used
|
||||||
}
|
|
||||||
self.next_write_pos = 0;
|
|
||||||
|
|
||||||
// The playback buffers don't need to be reset since they're always initialized before being
|
|
||||||
// used
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a sample to the buffer. The write position is advanced whenever the last channel is
|
/// Prepare the playback buffers to play back audio at the specified frequency. This resets the
|
||||||
/// written to.
|
/// buffer to record the next `note_period_samples`, which are then looped until the key is released.
|
||||||
pub fn push(&mut self, channel_idx: usize, sample: f32) {
|
pub fn prepare_playback(&mut self, frequency: f32) {
|
||||||
self.recording_buffers[channel_idx][self.next_write_pos] = sample;
|
let note_period_samples = (frequency.recip() * self.sample_rate).ceil() as usize;
|
||||||
|
|
||||||
|
// This buffer doesn't need to be cleared since the data is not read until the entire buffer
|
||||||
|
// has been recorded to
|
||||||
|
nih_debug_assert!(note_period_samples <= self.audio_buffers[0].capacity());
|
||||||
|
for buffer in self.audio_buffers.iter_mut() {
|
||||||
|
buffer.resize(note_period_samples, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The buffer is filled on
|
||||||
|
self.next_sample_pos = 0;
|
||||||
|
self.playback_buffer_ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read or write a sample from or to the ring buffer, and return the output. On the first loop
|
||||||
|
/// this will store the input samples into the bufffer and return the input value as is.
|
||||||
|
/// Afterwards it will read the previously recorded data from the buffer. The read/write
|
||||||
|
/// position is advanced whenever the last channel is written to.
|
||||||
|
pub fn next_sample(
|
||||||
|
&mut self,
|
||||||
|
channel_idx: usize,
|
||||||
|
input_sample: f32,
|
||||||
|
normalization_mode: NormalizationMode,
|
||||||
|
) -> f32 {
|
||||||
|
if !self.playback_buffer_ready {
|
||||||
|
self.audio_buffers[channel_idx][self.next_sample_pos] = input_sample;
|
||||||
|
}
|
||||||
|
let result = self.audio_buffers[channel_idx][self.next_sample_pos];
|
||||||
|
|
||||||
// TODO: This can be done more efficiently, but you really won't notice the performance
|
// TODO: This can be done more efficiently, but you really won't notice the performance
|
||||||
// impact here
|
// impact here
|
||||||
if channel_idx == self.recording_buffers.len() - 1 {
|
if channel_idx == self.audio_buffers.len() - 1 {
|
||||||
self.next_write_pos += 1;
|
self.next_sample_pos += 1;
|
||||||
|
|
||||||
if self.next_write_pos == self.recording_buffers[0].len() {
|
if self.next_sample_pos == self.audio_buffers[0].len() {
|
||||||
self.next_write_pos = 0;
|
self.next_sample_pos = 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepare the playback buffers to play back audio at the specified frequency. This copies
|
// The playback buffer is normalized as necessary. This prevents small grains from being
|
||||||
/// audio from the ring buffers to the playback buffers.
|
// either way quieter or way louder than the origianl audio.
|
||||||
pub fn prepare_playback(&mut self, frequency: f32, normalization_mode: NormalizationMode) {
|
if !self.playback_buffer_ready {
|
||||||
let note_period_samples = (frequency.recip() * self.sample_rate).ceil() as usize;
|
match normalization_mode {
|
||||||
|
NormalizationMode::None => (),
|
||||||
|
NormalizationMode::Auto => {
|
||||||
|
// FIXME: This needs to take the input audio into account, but we don't
|
||||||
|
// have access to that anymore. We can just use a simple envelope
|
||||||
|
// follower instead
|
||||||
|
// // Prevent this from causing divisions by zero or making very loud clicks when audio
|
||||||
|
// // playback has just started
|
||||||
|
// let playback_rms = calculate_rms(&self.playback_buffers);
|
||||||
|
// if playback_rms > 0.001 {
|
||||||
|
// let recording_rms = calculate_rms(&self.recording_buffers);
|
||||||
|
// let normalization_factor = recording_rms / playback_rms;
|
||||||
|
|
||||||
// We'll copy the last `note_period_samples` samples from the recording ring buffers to the
|
// for buffer in self.playback_buffers.iter_mut() {
|
||||||
// playback buffers
|
// for sample in buffer.iter_mut() {
|
||||||
nih_debug_assert!(note_period_samples <= self.playback_buffers[0].capacity());
|
// *sample *= normalization_factor;
|
||||||
for (playback_buffer, recording_buffer) in self
|
// }
|
||||||
.playback_buffers
|
// }
|
||||||
.iter_mut()
|
// }
|
||||||
.zip(self.recording_buffers.iter())
|
|
||||||
{
|
|
||||||
playback_buffer.resize(note_period_samples, 0.0);
|
|
||||||
|
|
||||||
// Keep in mind we'll need to go `note_period_samples` samples backwards in the
|
|
||||||
// recording buffer
|
|
||||||
let copy_num_from_start = usize::min(note_period_samples, self.next_write_pos);
|
|
||||||
let copy_num_from_end = note_period_samples - copy_num_from_start;
|
|
||||||
playback_buffer[0..copy_num_from_end]
|
|
||||||
.copy_from_slice(&recording_buffer[recording_buffer.len() - copy_num_from_end..]);
|
|
||||||
playback_buffer[copy_num_from_end..].copy_from_slice(
|
|
||||||
&recording_buffer[self.next_write_pos - copy_num_from_start..self.next_write_pos],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 => {
|
|
||||||
// Prevent this from causing divisions by zero or making very loud clicks when audio
|
|
||||||
// playback has just started
|
|
||||||
let playback_rms = calculate_rms(&self.playback_buffers);
|
|
||||||
if playback_rms > 0.001 {
|
|
||||||
let recording_rms = calculate_rms(&self.recording_buffers);
|
|
||||||
let normalization_factor = recording_rms / playback_rms;
|
|
||||||
|
|
||||||
for buffer in self.playback_buffers.iter_mut() {
|
|
||||||
for sample in buffer.iter_mut() {
|
|
||||||
*sample *= normalization_factor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At this point the buffer is ready for playback
|
||||||
|
self.playback_buffer_ready = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reading from the buffer should always start at the beginning
|
result
|
||||||
self.playback_buffer_pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a sample from the playback buffer. The playback position is advanced whenever the
|
|
||||||
/// last channel is written to. When the playback position reaches the end of the buffer it
|
|
||||||
/// wraps around.
|
|
||||||
pub fn next_playback_sample(&mut self, channel_idx: usize) -> f32 {
|
|
||||||
let sample = self.playback_buffers[channel_idx][self.playback_buffer_pos];
|
|
||||||
|
|
||||||
// TODO: Same as the above
|
|
||||||
if channel_idx == self.playback_buffers.len() - 1 {
|
|
||||||
self.playback_buffer_pos += 1;
|
|
||||||
|
|
||||||
if self.playback_buffer_pos == self.playback_buffers[0].len() {
|
|
||||||
self.playback_buffer_pos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,8 +27,7 @@ struct BuffrGlitch {
|
||||||
params: Arc<BuffrGlitchParams>,
|
params: Arc<BuffrGlitchParams>,
|
||||||
|
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
/// The ring buffer we'll write samples to. When a key is held down, we'll stop writing samples
|
/// The ring buffer samples are recorded to and played back from when a key is held down.
|
||||||
/// and instead keep reading from this buffer until the key is released.
|
|
||||||
buffer: buffer::RingBuffer,
|
buffer: buffer::RingBuffer,
|
||||||
|
|
||||||
/// The MIDI note ID of the last note, if a note pas pressed.
|
/// The MIDI note ID of the last note, if a note pas pressed.
|
||||||
|
@ -39,9 +38,10 @@ struct BuffrGlitch {
|
||||||
|
|
||||||
#[derive(Params)]
|
#[derive(Params)]
|
||||||
struct BuffrGlitchParams {
|
struct BuffrGlitchParams {
|
||||||
/// Controls if and how grains are normalization.
|
// FIXME: Add normalization back in, it doesn't work anymore so it's been removed to avoid causing confusion
|
||||||
#[id = "normalization_mode"]
|
// /// Controls whether and how grains are normalization.
|
||||||
normalization_mode: EnumParam<NormalizationMode>,
|
// #[id = "normalization_mode"]
|
||||||
|
// normalization_mode: EnumParam<NormalizationMode>,
|
||||||
/// From 0 to 1, how much of the dry signal to mix in. This defaults to 1 but it can be turned
|
/// From 0 to 1, how much of the dry signal to mix in. This defaults to 1 but it can be turned
|
||||||
/// down to use Buffr Glitch as more of a synth.
|
/// down to use Buffr Glitch as more of a synth.
|
||||||
#[id = "dry_mix"]
|
#[id = "dry_mix"]
|
||||||
|
@ -81,7 +81,7 @@ 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),
|
// normalization_mode: EnumParam::new("Normalization", NormalizationMode::Auto),
|
||||||
dry_level: FloatParam::new(
|
dry_level: FloatParam::new(
|
||||||
"Dry Level",
|
"Dry Level",
|
||||||
1.0,
|
1.0,
|
||||||
|
@ -181,10 +181,7 @@ impl Plugin for BuffrGlitch {
|
||||||
// larger window sizes.
|
// larger window sizes.
|
||||||
let note_frequency = util::midi_note_to_freq(note)
|
let note_frequency = util::midi_note_to_freq(note)
|
||||||
* 2.0f32.powi(self.params.octave_shift.value());
|
* 2.0f32.powi(self.params.octave_shift.value());
|
||||||
self.buffer.prepare_playback(
|
self.buffer.prepare_playback(note_frequency);
|
||||||
note_frequency,
|
|
||||||
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
|
||||||
|
@ -198,19 +195,22 @@ impl Plugin for BuffrGlitch {
|
||||||
|
|
||||||
// When a note is being held, we'll replace the input audio with the looping contents of
|
// When a note is being held, we'll replace the input audio with the looping contents of
|
||||||
// the playback buffer
|
// the playback buffer
|
||||||
|
// TODO: At some point also handle polyphony here
|
||||||
if self.midi_note_id.is_some() {
|
if self.midi_note_id.is_some() {
|
||||||
for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
|
for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
|
||||||
// New audio still needs to be recorded when the note is held to prepare for new
|
// This will start recording on the first iteration, and then loop the recorded
|
||||||
// notes
|
// buffer afterwards
|
||||||
// TODO: At some point also handle polyphony here
|
*sample = self.buffer.next_sample(
|
||||||
self.buffer.push(channel_idx, *sample);
|
channel_idx,
|
||||||
|
*sample,
|
||||||
*sample = self.buffer.next_playback_sample(channel_idx);
|
// FIXME: This has temporarily been removed, and `NormalizationMode::Auto`
|
||||||
|
// doesn't do anything right now
|
||||||
|
// self.params.normalization_mode.value(),
|
||||||
|
NormalizationMode::Auto,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
|
for sample in channel_samples.into_iter() {
|
||||||
self.buffer.push(channel_idx, *sample);
|
|
||||||
|
|
||||||
*sample *= dry_amount;
|
*sample *= dry_amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue