Browse Source

Prototypical implementation of a stutter effect

The VST effect already does what it is supposed to be doing, i.e.
looping small chunks of the input audio when it is active.

There are some downsides of the current implementation, e.g. that the
buffer is not filled when the effect is in active mode because there is
only one buffer and it is be read from in looping mode. This means that
the buffer does not get updated as long as the looping is active and
thus switching it off and on quickly would output old audio data.

Another approach would be to have two ring-buffers, the first is filled
with the input signal unconditionally whereas the second buffer is a copy of
the first one at the moment of switching on the effect. This would also
simplify the implementation because the second buffer is always read
from the first index.
The copy operation has to be very fast but this should be no problem as
long as `copy_from_slice` is used.

Some nice additions would be:

- set sample length in beat measures, milliseconds and samples
- set playback speed (requires interpolation (linear?))
- allow to reverse audio

PS: Cross compiling for windows by building against the x86_64-pc-windows-gnu
target showed that it runs on Windows as well.
master
Andreas Linz 2 years ago
commit
eab422d665
Signed by: alinz GPG Key ID: 9BF39809C9DA580C
3 changed files with 187 additions and 0 deletions
  1. +4
    -0
      .gitignore
  2. +11
    -0
      Cargo.toml
  3. +172
    -0
      src/lib.rs

+ 4
- 0
.gitignore View File

@ -0,0 +1,4 @@
/target/
/.idea/
/**/*.rs.bk
/Cargo.lock

+ 11
- 0
Cargo.toml View File

@ -0,0 +1,11 @@
[package]
name = "rutter"
version = "0.1.0"
authors = ["Andreas Linz <klingt.net@gmail.com>"]
[dependencies]
vst = { git = "https://github.com/rust-dsp/rust-vst" }
[lib]
name = "rutter"
crate-type = ["cdylib"]

+ 172
- 0
src/lib.rs View File

@ -0,0 +1,172 @@
#[macro_use]
extern crate vst;
use vst::buffer::AudioBuffer;
use vst::plugin::{Category, HostCallback, Info, Plugin};
type Frame = (f32, f32);
#[derive(Default)]
struct Rutter {
buf: Vec<Frame>,
loop_length: u32,
active: bool,
// position inside the buffer
write_index: usize,
read_index: usize,
}
impl Rutter {
fn is_stereo(&self, buf: &AudioBuffer<f32>) -> bool {
buf.input_count() == 2 && buf.output_count() == 2
}
fn is_not_stereo(&self, buf: &AudioBuffer<f32>) -> bool {
!self.is_stereo(buf)
}
}
impl Plugin for Rutter {
fn new(host: HostCallback) -> Self {
Rutter {
buf: vec![(0.0, 0.0); 48_000],
loop_length: 1_000,
active: false,
write_index: 0,
read_index: 0,
}
}
fn get_info(&self) -> Info {
Info {
name: "rutter".into(),
vendor: "klingt.net".into(),
unique_id: 788968228,
category: Category::Effect,
inputs: 2,
outputs: 2,
parameters: 2,
..Default::default()
}
}
fn get_parameter(&self, index: i32) -> f32 {
match index {
0 => (self.active as u32) as f32,
1 => self.loop_length as f32,
_ => 0.0,
}
}
fn set_parameter(&mut self, index: i32, val: f32) {
match index {
0 => self.active = val >= 1.0,
1 => self.loop_length = (val * self.buf.len() as f32) as u32, // set min length
_ => (),
}
}
fn get_parameter_text(&self, index: i32) -> String {
match index {
0 => if self.active { "on" } else { "off" }.into(),
1 => format!("{:.2}smpls", (self.loop_length)),
_ => "".into(),
}
}
// This shows the control's name.
fn get_parameter_name(&self, index: i32) -> String {
match index {
0 => "Active",
1 => "Loop Length",
_ => "",
}.to_string()
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
if self.is_not_stereo(buffer) {
return;
}
// destructure
let buffer_samples = buffer.samples();
let (inb, outb) = buffer.split();
let in_l = inb.get(0);
let in_r = inb.get(1);
let out_l = outb.get_mut(0);
let out_r = outb.get_mut(1);
let space_left = self.buf.len() as isize - (self.write_index + buffer_samples) as isize;
// do not write into buffer when active
// TODO: maybe copy looped slice out of internal buffer when active
if !self.active {
if space_left > 0 {
for (i, (l, r)) in
(self.write_index..self.write_index + buffer_samples).zip(in_l.iter().zip(in_r))
{
// TODO: use vec.splice to replace parts
self.buf[i] = (*l, *r);
}
self.write_index += buffer_samples;
} else {
// wrap around
for (i, (l, r)) in (self.write_index..self.buf.len())
.chain(0..space_left.abs() as usize)
.zip(in_l.iter().zip(in_r))
{
self.buf[i] = (*l, *r);
}
self.write_index = space_left.abs() as usize;
}
self.read_index = self.write_index;
}
// if active read from internal buffer otherwise copy through
if self.active {
// here should be some loop logic:
// - loop length
// - playback speed?
// - reverse
// - modify a running index (`buffer` is usually smaller than the internal buffer)
// - start from last index (and wrap-around)
let wrap_around = self.read_index + buffer_samples > self.buf.len();
if wrap_around {
let wrap_end = (self.read_index + buffer_samples) - self.buf.len();
for (i, (ol, or)) in (self.read_index..self.buf.len())
.chain(0..wrap_end)
.zip(out_l.iter_mut().zip(out_r))
{
*ol = self.buf[i].0;
*or = self.buf[i].1;
}
if self.loop_length as usize - (self.buf.len() - self.write_index) < wrap_end {
self.read_index = self.write_index;
} else {
self.read_index = wrap_end;
}
} else {
for (i, (ol, or)) in (self.read_index..self.read_index + buffer_samples)
.zip(out_l.iter_mut().zip(out_r))
{
*ol = self.buf[i].0;
*or = self.buf[i].1;
}
if self.read_index - self.write_index > self.loop_length as usize {
self.read_index = self.write_index;
} else {
self.read_index += buffer_samples;
}
}
} else {
// write through
for ((li, ri), (lo, ro)) in in_l.iter().zip(in_r).zip(out_l.iter_mut().zip(out_r)) {
*lo = *li;
*ro = *ri;
}
}
}
}
plugin_main!(Rutter);

Loading…
Cancel
Save