diff --git a/plugins/buffr_glitch/src/buffer.rs b/plugins/buffr_glitch/src/buffer.rs
index ceaa9f2b..215da35a 100644
--- a/plugins/buffr_glitch/src/buffer.rs
+++ b/plugins/buffr_glitch/src/buffer.rs
@@ -14,18 +14,29 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use nih_plug::prelude::util;
+use nih_plug::prelude::*;
-/// A super simple ring buffer abstraction to store the last received audio. This needs to be able
-/// to store at least the number of samples that correspond to the period size of MIDI note 0.
+/// 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
+/// samples that correspond to the period size of MIDI note 0.
#[derive(Debug, Default)]
pub struct RingBuffer {
- /// Sample buffers indexed by channel and sample index.
- buffers: Vec>,
+ sample_rate: f32,
+
+ /// Sample ring buffers indexed by channel and sample index. These are always recorded to.
+ recording_buffers: Vec>,
/// The positions within the sample buffers the next sample should be written to. Since all
/// channels will be written to in lockstep we only need a single value here. It's incremented
/// when writing a sample for the last channel.
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>,
+ /// The current playback position in `playback_buffers`.
+ playback_buffer_pos: usize,
}
impl RingBuffer {
@@ -38,34 +49,93 @@ impl RingBuffer {
let note_period_samples = (note_frequency.recip() * sample_rate).ceil() as usize;
let buffer_len = note_period_samples.next_power_of_two();
- self.buffers.resize_with(num_channels, Vec::new);
- for buffer in self.buffers.iter_mut() {
+ // Used later to compute period sizes in samples based on frequencies
+ self.sample_rate = sample_rate;
+
+ self.recording_buffers.resize_with(num_channels, Vec::new);
+ for buffer in self.recording_buffers.iter_mut() {
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.
pub fn reset(&mut self) {
- for buffer in self.buffers.iter_mut() {
+ for buffer in self.recording_buffers.iter_mut() {
buffer.fill(0.0);
}
-
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
/// written to.
pub fn push(&mut self, channel_idx: usize, sample: f32) {
- self.buffers[channel_idx][self.next_write_pos] = sample;
+ self.recording_buffers[channel_idx][self.next_write_pos] = sample;
// TODO: This can be done more efficiently, but you really won't notice the performance
// impact here
- if channel_idx == self.buffers.len() - 1 {
+ if channel_idx == self.recording_buffers.len() - 1 {
self.next_write_pos += 1;
- if self.next_write_pos == self.buffers[0].len() {
+ if self.next_write_pos == self.recording_buffers[0].len() {
self.next_write_pos = 0;
}
}
}
+
+ /// 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) {
+ 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
+ // playback buffers
+ nih_debug_assert!(note_period_samples <= self.playback_buffers[0].capacity());
+ 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[..copy_num_from_start]);
+ }
+
+ // Reading from the buffer should always start at the beginning
+ 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
+ }
}
diff --git a/plugins/buffr_glitch/src/lib.rs b/plugins/buffr_glitch/src/lib.rs
index 72bb6974..49bf885b 100644
--- a/plugins/buffr_glitch/src/lib.rs
+++ b/plugins/buffr_glitch/src/lib.rs
@@ -26,8 +26,14 @@ struct BuffrGlitch {
/// The ring buffer we'll write samples to. When a key is held down, we'll stop writing samples
/// and instead keep reading from this buffer until the key is released.
buffer: buffer::RingBuffer,
+
+ /// The MIDI note ID of the last note, if a note pas pressed.
+ //
+ // TODO: Add polyphony support, this is just a quick proof of concept.
+ midi_note_id: Option,
}
+// TODO: Normalize option
#[derive(Params)]
struct BuffrGlitchParams {}
@@ -38,6 +44,8 @@ impl Default for BuffrGlitch {
sample_rate: 1.0,
buffer: buffer::RingBuffer::default(),
+
+ midi_note_id: None,
}
}
}
@@ -88,17 +96,61 @@ impl Plugin for BuffrGlitch {
fn reset(&mut self) {
self.buffer.reset();
+ self.midi_note_id = None;
}
fn process(
&mut self,
buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers,
- _context: &mut impl ProcessContext,
+ context: &mut impl ProcessContext,
) -> ProcessStatus {
- for channel_samples in buffer.iter_samples() {
- for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
- self.buffer.push(channel_idx, *sample);
+ let mut next_event = context.next_event();
+
+ for (sample_idx, channel_samples) in buffer.iter_samples().enumerate() {
+ // TODO: Split blocks based on events when adding polyphony, this is just a simple proof
+ // of concept
+ while let Some(event) = next_event {
+ if event.timing() > sample_idx as u32 {
+ break;
+ }
+
+ match event {
+ NoteEvent::NoteOn { note, .. } => {
+ // We don't keep a stack of notes right now. At some point we'll want to
+ // make this polyphonic anyways.
+ // TOOD: Also add an option to use velocity or poly pressure
+ self.midi_note_id = Some(note);
+
+ // 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));
+ }
+ NoteEvent::NoteOff { note, .. } if self.midi_note_id == Some(note) => {
+ // A NoteOff for the currently playing note immediately ends playback
+ self.midi_note_id = None;
+ }
+ _ => (),
+ }
+
+ next_event = context.next_event();
+ }
+
+ // When a note is being held, we'll replace the input audio with the looping contents of
+ // the playback buffer
+ if self.midi_note_id.is_some() {
+ 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
+ // notes
+ // TODO: At some point also handle polyphony here
+ self.buffer.push(channel_idx, *sample);
+
+ *sample = self.buffer.next_playback_sample(channel_idx);
+ }
+ } else {
+ for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
+ self.buffer.push(channel_idx, *sample);
+ }
}
}