diff --git a/plugins/buffr_glitch/CHANGELOG.md b/plugins/buffr_glitch/CHANGELOG.md index a3e31d22..90493f66 100644 --- a/plugins/buffr_glitch/CHANGELOG.md +++ b/plugins/buffr_glitch/CHANGELOG.md @@ -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 diff --git a/plugins/buffr_glitch/src/buffer.rs b/plugins/buffr_glitch/src/buffer.rs index 1dd19d80..da2c90a5 100644 --- a/plugins/buffr_glitch/src/buffer.rs +++ b/plugins/buffr_glitch/src/buffer.rs @@ -33,10 +33,26 @@ pub struct RingBuffer { audio_buffers: Vec>, /// 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, + }; } } diff --git a/plugins/buffr_glitch/src/lib.rs b/plugins/buffr_glitch/src/lib.rs index 8e25816b..a3620cbb 100644 --- a/plugins/buffr_glitch/src/lib.rs +++ b/plugins/buffr_glitch/src/lib.rs @@ -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