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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue