1
0
Fork 0

Add a crossfade option to Buffr Glitch

This commit is contained in:
Robbert van der Helm 2023-01-17 00:30:48 +01:00
parent 1e90f55019
commit 22b3b9527b
3 changed files with 75 additions and 15 deletions

View file

@ -10,6 +10,8 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Added ### Added
- Added an option to crossfade the recorded buffer to avoid clicks when looping
around.
- Added an optional velocity sensitive mode. - Added an optional velocity sensitive mode.
### Removed ### Removed

View file

@ -33,10 +33,26 @@ pub struct RingBuffer {
audio_buffers: Vec<Vec<f32>>, audio_buffers: Vec<Vec<f32>>,
/// The current playback position in `playback_buffers`. /// The current playback position in `playback_buffers`.
next_sample_pos: usize, next_sample_pos: usize,
/// If this is set to `false` then the incoming audio will be recorded to `playback_buffer` /// The length of the crossfade, in samples. After the first this additional samples are
/// until it is full. When it wraps around this is set to `true` and the previously recorded /// recorded and faded back into the buffer.
/// audio is played back instead. crossfade_length: usize,
playback_buffer_ready: bool, /// See [`BufferStatus`].
buffer_status: BufferStatus,
}
#[derive(Debug, Default, Clone, Copy)]
enum BufferStatus {
/// The buffer has not yet been filled and all sample should be recorded into the buffer.
#[default]
Recording,
/// The buffer has wrapped around, but `crossfade_length` is set to 1 or more samples. This
/// second pass continues recording, and replaces the buffer's start with a cross faded version
/// of that input and the existing contents.
Crossfading,
/// The buffer has wrapped around once and `crossfade_length` is set to 0, or it has wrapped
/// around twice and crossfading is enabled. Samples only need to be read from the buffer, all
/// work is done.
Ready,
} }
impl RingBuffer { impl RingBuffer {
@ -71,8 +87,12 @@ impl RingBuffer {
} }
/// Prepare the playback buffers to play back audio at the specified frequency. This resets the /// Prepare the playback buffers to play back audio at the specified frequency. This resets the
/// buffer to record the next `note_period_samples`, which are then looped until the key is released. /// buffer to record the next `note_period_samples`, which are then looped until the key is
pub fn prepare_playback(&mut self, frequency: f32) { /// released. The crossfade length is also set at this point since right now we don't record
/// more than necessary and can't change this afterwards.
pub fn prepare_playback(&mut self, frequency: f32, crossfade_ms: f32) {
nih_debug_assert!(frequency > 0.0);
nih_debug_assert!(crossfade_ms >= 0.0);
let note_period_samples = (frequency.recip() * self.sample_rate).ceil() as usize; 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 // This buffer doesn't need to be cleared since the data is not read until the entire buffer
@ -82,9 +102,12 @@ impl RingBuffer {
buffer.resize(note_period_samples, 0.0); buffer.resize(note_period_samples, 0.0);
} }
// The buffer is filled on // The buffer is filled on the first `note_period_samples` calls to `next_sample`, plus a
// little more for the crossfade if set
self.next_sample_pos = 0; self.next_sample_pos = 0;
self.playback_buffer_ready = false; self.crossfade_length =
((crossfade_ms * self.sample_rate).ceil() as usize).min(note_period_samples);
self.buffer_status = BufferStatus::Recording;
} }
/// Read or write a sample from or to the ring buffer, and return the output. On the first loop /// Read or write a sample from or to the ring buffer, and return the output. On the first loop
@ -92,8 +115,21 @@ impl RingBuffer {
/// Afterwards it will read the previously recorded data from the buffer. The read/write /// Afterwards it will read the previously recorded data from the buffer. The read/write
/// position is advanced whenever the last channel is written to. /// position is advanced whenever the last channel is written to.
pub fn next_sample(&mut self, channel_idx: usize, input_sample: f32) -> f32 { pub fn next_sample(&mut self, channel_idx: usize, input_sample: f32) -> f32 {
if !self.playback_buffer_ready { match self.buffer_status {
self.audio_buffers[channel_idx][self.next_sample_pos] = input_sample; BufferStatus::Recording => {
self.audio_buffers[channel_idx][self.next_sample_pos] = input_sample
}
BufferStatus::Crossfading if self.next_sample_pos < self.crossfade_length => {
// This is an equal power fade between the part of the input after the first loop
// and the buffer's existing contents.
let crossfade_t = self.next_sample_pos as f32 / (self.crossfade_length - 1) as f32;
let new_t = (1.0 - crossfade_t).sqrt();
let existing_t = crossfade_t.sqrt();
self.audio_buffers[channel_idx][self.next_sample_pos] = (input_sample * new_t)
+ (self.audio_buffers[channel_idx][self.next_sample_pos] * existing_t);
}
_ => (),
} }
let result = self.audio_buffers[channel_idx][self.next_sample_pos]; let result = self.audio_buffers[channel_idx][self.next_sample_pos];
@ -105,8 +141,12 @@ impl RingBuffer {
if self.next_sample_pos == self.audio_buffers[0].len() { if self.next_sample_pos == self.audio_buffers[0].len() {
self.next_sample_pos = 0; self.next_sample_pos = 0;
// At this point the buffer is ready for playback self.buffer_status = match self.buffer_status {
self.playback_buffer_ready = true; BufferStatus::Recording if self.crossfade_length > 0 => {
BufferStatus::Crossfading
}
_ => BufferStatus::Ready,
};
} }
} }

View file

@ -50,11 +50,17 @@ struct BuffrGlitchParams {
/// Makes the effect velocity sensitive. `100/127` corresponds to `1.0` gain. /// Makes the effect velocity sensitive. `100/127` corresponds to `1.0` gain.
#[id = "velocity_sensitive"] #[id = "velocity_sensitive"]
velocity_sensitive: BoolParam, velocity_sensitive: BoolParam,
/// The number of octaves the input signal should be increased or decreased by. Useful to allow /// The number of octaves the input signal should be increased or decreased by. Useful to allow
/// larger grain sizes. /// larger grain sizes.
#[id = "octave_shift"] #[id = "octave_shift"]
octave_shift: IntParam, octave_shift: IntParam,
/// The length of the loop crossfade to use, in milliseconds. This will cause the start of the
/// loop to be faded into the last `(crossfade_ms/2)` ms of the loop region, and the part after
/// the end to be faded into the first `(crossfade_ms/2)` ms of the loop after the first
/// ieration.
#[id = "crossfade_ms"]
crossfade_ms: FloatParam,
} }
impl Default for BuffrGlitch { impl Default for BuffrGlitch {
@ -89,7 +95,6 @@ impl Default for BuffrGlitchParams {
.with_value_to_string(formatters::v2s_f32_gain_to_db(1)) .with_value_to_string(formatters::v2s_f32_gain_to_db(1))
.with_string_to_value(formatters::s2v_f32_gain_to_db()), .with_string_to_value(formatters::s2v_f32_gain_to_db()),
velocity_sensitive: BoolParam::new("Velocity Sensitive", false), velocity_sensitive: BoolParam::new("Velocity Sensitive", false),
octave_shift: IntParam::new( octave_shift: IntParam::new(
"Octave Shift", "Octave Shift",
0, 0,
@ -98,6 +103,18 @@ impl Default for BuffrGlitchParams {
max: MAX_OCTAVE_SHIFT as i32, max: MAX_OCTAVE_SHIFT as i32,
}, },
), ),
crossfade_ms: FloatParam::new(
"Crossfade",
0.0,
FloatRange::Skewed {
min: 0.0,
max: 50.0,
factor: FloatRange::skew_factor(-2.5),
},
)
// This doesn't need smoothing because the value is set when the note is held down and cannot be changed afterwards
.with_unit(" ms")
.with_step_size(0.001),
} }
} }
} }
@ -180,7 +197,8 @@ 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(note_frequency); self.buffer
.prepare_playback(note_frequency, self.params.crossfade_ms.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