Add the MIDI playback to Buffr Glitch
This commit is contained in:
parent
ea61947f1d
commit
7c04ec856f
|
@ -14,18 +14,29 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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
|
/// A super simple ring buffer abstraction that records audio into a recording ring buffer, and then
|
||||||
/// to store at least the number of samples that correspond to the period size of MIDI note 0.
|
/// 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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RingBuffer {
|
pub struct RingBuffer {
|
||||||
/// Sample buffers indexed by channel and sample index.
|
sample_rate: f32,
|
||||||
buffers: Vec<Vec<f32>>,
|
|
||||||
|
/// Sample ring buffers indexed by channel and sample index. These are always recorded to.
|
||||||
|
recording_buffers: Vec<Vec<f32>>,
|
||||||
/// The positions within the sample buffers the next sample should be written to. Since all
|
/// 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
|
/// 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.
|
/// when writing a sample for the last channel.
|
||||||
next_write_pos: usize,
|
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`.
|
||||||
|
playback_buffer_pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RingBuffer {
|
impl RingBuffer {
|
||||||
|
@ -38,34 +49,93 @@ impl RingBuffer {
|
||||||
let note_period_samples = (note_frequency.recip() * sample_rate).ceil() as usize;
|
let note_period_samples = (note_frequency.recip() * sample_rate).ceil() as usize;
|
||||||
let buffer_len = note_period_samples.next_power_of_two();
|
let buffer_len = note_period_samples.next_power_of_two();
|
||||||
|
|
||||||
self.buffers.resize_with(num_channels, Vec::new);
|
// Used later to compute period sizes in samples based on frequencies
|
||||||
for buffer in self.buffers.iter_mut() {
|
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);
|
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.buffers.iter_mut() {
|
for buffer in self.recording_buffers.iter_mut() {
|
||||||
buffer.fill(0.0);
|
buffer.fill(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.next_write_pos = 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
|
/// Push a sample to the buffer. The write position is advanced whenever the last channel is
|
||||||
/// written to.
|
/// written to.
|
||||||
pub fn push(&mut self, channel_idx: usize, sample: f32) {
|
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
|
// TODO: This can be done more efficiently, but you really won't notice the performance
|
||||||
// impact here
|
// impact here
|
||||||
if channel_idx == self.buffers.len() - 1 {
|
if channel_idx == self.recording_buffers.len() - 1 {
|
||||||
self.next_write_pos += 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;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
/// 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.
|
/// 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.
|
||||||
|
//
|
||||||
|
// TODO: Add polyphony support, this is just a quick proof of concept.
|
||||||
|
midi_note_id: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Normalize option
|
||||||
#[derive(Params)]
|
#[derive(Params)]
|
||||||
struct BuffrGlitchParams {}
|
struct BuffrGlitchParams {}
|
||||||
|
|
||||||
|
@ -38,6 +44,8 @@ impl Default for BuffrGlitch {
|
||||||
|
|
||||||
sample_rate: 1.0,
|
sample_rate: 1.0,
|
||||||
buffer: buffer::RingBuffer::default(),
|
buffer: buffer::RingBuffer::default(),
|
||||||
|
|
||||||
|
midi_note_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,17 +96,61 @@ impl Plugin for BuffrGlitch {
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.buffer.reset();
|
self.buffer.reset();
|
||||||
|
self.midi_note_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut Buffer,
|
buffer: &mut Buffer,
|
||||||
_aux: &mut AuxiliaryBuffers,
|
_aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext<Self>,
|
context: &mut impl ProcessContext<Self>,
|
||||||
) -> ProcessStatus {
|
) -> ProcessStatus {
|
||||||
for channel_samples in buffer.iter_samples() {
|
let mut next_event = context.next_event();
|
||||||
for (channel_idx, sample) in channel_samples.into_iter().enumerate() {
|
|
||||||
self.buffer.push(channel_idx, *sample);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue