From 7c04ec856f268ce333e2a58649ea9634d775ada3 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 9 Nov 2022 17:17:51 +0100 Subject: [PATCH] Add the MIDI playback to Buffr Glitch --- plugins/buffr_glitch/src/buffer.rs | 94 ++++++++++++++++++++++++++---- plugins/buffr_glitch/src/lib.rs | 60 +++++++++++++++++-- 2 files changed, 138 insertions(+), 16 deletions(-) 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); + } } }