Add a crossfade option to Buffr Glitch
This commit is contained in:
parent
1e90f55019
commit
22b3b9527b
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue