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 an option to crossfade the recorded buffer to avoid clicks when looping
around.
- Added an optional velocity sensitive mode.
### Removed

View file

@ -33,10 +33,26 @@ pub struct RingBuffer {
audio_buffers: Vec<Vec<f32>>,
/// The current playback position in `playback_buffers`.
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,
/// The length of the crossfade, in samples. After the first this additional samples are
/// recorded and faded back into the buffer.
crossfade_length: usize,
/// 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 {
@ -71,8 +87,12 @@ impl RingBuffer {
}
/// 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.
pub fn prepare_playback(&mut self, frequency: f32) {
/// buffer to record the next `note_period_samples`, which are then looped until the key is
/// 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;
// 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);
}
// 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.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
@ -92,8 +115,21 @@ impl RingBuffer {
/// 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) -> f32 {
if !self.playback_buffer_ready {
self.audio_buffers[channel_idx][self.next_sample_pos] = input_sample;
match self.buffer_status {
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];
@ -105,8 +141,12 @@ impl RingBuffer {
if self.next_sample_pos == self.audio_buffers[0].len() {
self.next_sample_pos = 0;
// At this point the buffer is ready for playback
self.playback_buffer_ready = true;
self.buffer_status = match self.buffer_status {
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.
#[id = "velocity_sensitive"]
velocity_sensitive: BoolParam,
/// The number of octaves the input signal should be increased or decreased by. Useful to allow
/// larger grain sizes.
#[id = "octave_shift"]
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 {
@ -89,7 +95,6 @@ impl Default for BuffrGlitchParams {
.with_value_to_string(formatters::v2s_f32_gain_to_db(1))
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
velocity_sensitive: BoolParam::new("Velocity Sensitive", false),
octave_shift: IntParam::new(
"Octave Shift",
0,
@ -98,6 +103,18 @@ impl Default for BuffrGlitchParams {
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.
let note_frequency = util::midi_note_to_freq(note)
* 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) => {
// A NoteOff for the currently playing note immediately ends playback