[sosten] add pitch detection
This commit is contained in:
parent
62bc461f60
commit
43605dfa87
4 changed files with 86 additions and 33 deletions
16
README.md
16
README.md
|
@ -48,6 +48,7 @@ PRs are welcome for more build instructions
|
||||||
|
|
||||||
the following is the current list of plugins
|
the following is the current list of plugins
|
||||||
|
|
||||||
|
|
||||||
- `basic_gain`: simple gain plugin
|
- `basic_gain`: simple gain plugin
|
||||||
- `noted`: output midi at regular intervals
|
- `noted`: output midi at regular intervals
|
||||||
- `sosten`: granular sustain plugin
|
- `sosten`: granular sustain plugin
|
||||||
|
@ -63,6 +64,8 @@ the following is the current list of plugins
|
||||||
- `double_reverse_delta_inverter`: idk, a weird distortion
|
- `double_reverse_delta_inverter`: idk, a weird distortion
|
||||||
- `transmute_pitch`: pitch to midi converter
|
- `transmute_pitch`: pitch to midi converter
|
||||||
|
|
||||||
|
there's a bit of an explanation of each of the plugins below, but it's not a thorough documentation or a manual, it's just a bunch of notes i've written and a short description of the parameters
|
||||||
|
|
||||||
### basic_gain
|
### basic_gain
|
||||||
|
|
||||||
simple gain plugin, used as a template for starting new projects
|
simple gain plugin, used as a template for starting new projects
|
||||||
|
@ -83,11 +86,18 @@ it's useful when you want to play a note super fast (over 1/64 tempo) but don't
|
||||||
sustains a sound by replaying a grain of sound on loop
|
sustains a sound by replaying a grain of sound on loop
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
- `length`: length of the grain in samples. maximum is 48000 cause i said so
|
|
||||||
- `mix`: dry/wet knob
|
|
||||||
- `enable`: will enable the sustain if it's over 0.5
|
- `enable`: will enable the sustain if it's over 0.5
|
||||||
|
- `length`: length of the grain in samples. maximum is 48000 cause i said so
|
||||||
|
- `manual/pitch detection`: whether to use the manually set length (if under 0.5) or use the detected pitch (over 0.5)
|
||||||
|
- `dissipation`: amount of dissipation of the input
|
||||||
|
|
||||||
to use this plugin, add an automation for `enable` and set the value to `1.0` wherever you want the sustain to happen
|
to use this plugin, add an automation for `enable` and set the value to `1.0` wherever you want the sustain to happen. as soon as that happens, it'll start looping the last `length` samples
|
||||||
|
|
||||||
|
if set to manual, it uses the provided length for the looped grain. if pitch detection is enabled, it will use the detected pitch to calculate the period of the input, and it'll use that for the length of the grain. this should cause the sound to be sustained seamlessly
|
||||||
|
|
||||||
|
dissipation is a weird thingy. it smooths out the sound, and i think it's a lowpass filter? not sure. makes cool sounds though. what it does is roughly `x[n] = dissipation * x[n] + (1 - dissipation) * x[n + 1]` after each time it plays a sample, so `dissipation = 1` will leave the audio untouched, and setting it to `0.5` provides the greatest effect
|
||||||
|
|
||||||
|
low lengths will almost produce a tone of frequency `1 / length`, which makes for interesting sounds
|
||||||
|
|
||||||
### quinoa [WIP]
|
### quinoa [WIP]
|
||||||
|
|
||||||
|
|
|
@ -4,20 +4,26 @@
|
||||||
use baseplug::{Plugin, ProcessContext};
|
use baseplug::{Plugin, ProcessContext};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use utils::buffers::*;
|
||||||
use utils::delay::*;
|
use utils::delay::*;
|
||||||
|
use utils::pitch::*;
|
||||||
|
|
||||||
// If you change this remember to change the max on the model
|
// If you change this remember to change the max on the model
|
||||||
const LEN: usize = 48000;
|
const LEN: usize = 48000;
|
||||||
|
const PITCH_LEN: usize = 2 << 9;
|
||||||
|
|
||||||
baseplug::model! {
|
baseplug::model! {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct SostenModel {
|
struct SostenModel {
|
||||||
|
#[model(min = 0.0, max = 1.0)]
|
||||||
|
#[parameter(name = "enable")]
|
||||||
|
enable: f32,
|
||||||
#[model(min = 10.0, max = 48000.0)]
|
#[model(min = 10.0, max = 48000.0)]
|
||||||
#[parameter(name = "length")]
|
#[parameter(name = "length")]
|
||||||
length: f32,
|
length: f32,
|
||||||
#[model(min = 0.0, max = 1.0)]
|
#[model(min = 0.0, max = 1.0)]
|
||||||
#[parameter(name = "enable")]
|
#[parameter(name = "manual/pitch detection")]
|
||||||
enable: f32,
|
manual: f32,
|
||||||
#[model(min = 0.0, max = 1.0)]
|
#[model(min = 0.0, max = 1.0)]
|
||||||
#[parameter(name = "dissipation")]
|
#[parameter(name = "dissipation")]
|
||||||
dissipation: f32,
|
dissipation: f32,
|
||||||
|
@ -27,21 +33,26 @@ baseplug::model! {
|
||||||
impl Default for SostenModel {
|
impl Default for SostenModel {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
length: 1000.0,
|
|
||||||
enable: 0.0,
|
enable: 0.0,
|
||||||
|
length: 1000.0,
|
||||||
|
manual: 0.0,
|
||||||
dissipation: 1.0,
|
dissipation: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Sosten {
|
struct Sosten {
|
||||||
delay_l: DelayLine<LEN>,
|
delay: DelayLines<LEN>,
|
||||||
delay_r: DelayLine<LEN>,
|
buffers: Buffers<LEN>,
|
||||||
buffer_l: [f32; LEN],
|
|
||||||
buffer_r: [f32; LEN],
|
|
||||||
|
|
||||||
playing: bool,
|
playing: bool,
|
||||||
idx: usize,
|
|
||||||
|
detector_thread: pitch_detection::PitchDetectorThread<PITCH_LEN>,
|
||||||
|
/// Period of the thing we're currently repeating
|
||||||
|
used_period: Option<usize>,
|
||||||
|
/// Period of the processed input
|
||||||
|
/// We keep both so we can instantly switch
|
||||||
|
period: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for Sosten {
|
impl Plugin for Sosten {
|
||||||
|
@ -57,13 +68,14 @@ impl Plugin for Sosten {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn new(_sample_rate: f32, _model: &SostenModel) -> Self {
|
fn new(_sample_rate: f32, _model: &SostenModel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
delay_l: DelayLine::<LEN>::new(),
|
delay: DelayLines::<LEN>::new(),
|
||||||
delay_r: DelayLine::<LEN>::new(),
|
buffers: Buffers::new(),
|
||||||
buffer_l: [0.; LEN],
|
|
||||||
buffer_r: [0.; LEN],
|
|
||||||
|
|
||||||
playing: false,
|
playing: false,
|
||||||
idx: 0,
|
|
||||||
|
detector_thread: pitch_detection::PitchDetectorThread::<PITCH_LEN>::new(),
|
||||||
|
used_period: None,
|
||||||
|
period: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,19 +85,34 @@ impl Plugin for Sosten {
|
||||||
let output = &mut ctx.outputs[0].buffers;
|
let output = &mut ctx.outputs[0].buffers;
|
||||||
|
|
||||||
for i in 0..ctx.nframes {
|
for i in 0..ctx.nframes {
|
||||||
// Update delays
|
// update delays
|
||||||
self.delay_l.write_and_advance(input[0][i]);
|
self.delay.write_and_advance(input[0][i], input[1][i]);
|
||||||
self.delay_r.write_and_advance(input[1][i]);
|
|
||||||
|
// pass input to pitch detector, in mono
|
||||||
|
self.detector_thread.write(
|
||||||
|
0.5 * (input[0][i] + input[0][i]),
|
||||||
|
0.0,
|
||||||
|
ctx.sample_rate as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
// get pitch from detector thread
|
||||||
|
match self.detector_thread.try_get_pitch() {
|
||||||
|
Some((pitch, _)) => {
|
||||||
|
let sr = ctx.sample_rate;
|
||||||
|
self.period = pitch.map(|pitch| (sr / pitch).floor() as usize);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle playing according to `enable`
|
// Toggle playing according to `enable`
|
||||||
if model.enable[i] >= 0.5 {
|
if model.enable[i] >= 0.5 {
|
||||||
// If it wasn't playing before this, reload buffer
|
// If it wasn't playing before this, reload buffer
|
||||||
if !self.playing {
|
if !self.playing {
|
||||||
self.delay_l.read_slice(&mut self.buffer_l);
|
self.delay.read_to_buffers(&mut self.buffers);
|
||||||
self.delay_r.read_slice(&mut self.buffer_r);
|
self.buffers.reset();
|
||||||
self.idx = 0;
|
|
||||||
|
|
||||||
self.playing = true;
|
self.playing = true;
|
||||||
|
self.used_period = self.period;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.playing = false;
|
self.playing = false;
|
||||||
|
@ -94,28 +121,34 @@ impl Plugin for Sosten {
|
||||||
// Play the repeating part
|
// Play the repeating part
|
||||||
if self.playing {
|
if self.playing {
|
||||||
// Length of section to play
|
// Length of section to play
|
||||||
let len = model.length[i].trunc() as usize;
|
let len = if model.manual[i] < 0.5 {
|
||||||
|
model.length[i].trunc() as usize
|
||||||
|
} else {
|
||||||
|
self.used_period.unwrap_or(model.length[i].trunc() as usize)
|
||||||
|
};
|
||||||
|
|
||||||
// If len has changed, idx may have not, so we do the min so we don't go out of bounds
|
// If len has changed, idx may have not, so we do the min so we don't go out of bounds
|
||||||
let idx = self.idx.min(len - 1);
|
let idx = self.buffers.idx.min(len - 1);
|
||||||
|
|
||||||
// Play from Buffer
|
// Play from Buffer
|
||||||
output[0][i] = self.buffer_l[(LEN - len) + idx];
|
let (l, r) = self.buffers.read((LEN - len) + idx);
|
||||||
output[1][i] = self.buffer_r[(LEN - len) + idx];
|
|
||||||
|
output[0][i] = l;
|
||||||
|
output[1][i] = r;
|
||||||
|
|
||||||
// dissipates the audio in the buffer, idk
|
// dissipates the audio in the buffer, idk
|
||||||
// it adds a bit of the next sample to this one, which smooths it out
|
// it adds a bit of the next sample to this one, which smooths it out
|
||||||
let diss = model.dissipation[i];
|
let diss = model.dissipation[i];
|
||||||
let a = (LEN - len) + idx;
|
let a = (LEN - len) + idx;
|
||||||
self.buffer_l[a] *= diss;
|
self.buffers.l[a] *= diss;
|
||||||
self.buffer_r[a] *= diss;
|
self.buffers.r[a] *= diss;
|
||||||
self.buffer_l[a] += (1.0 - diss) * self.buffer_l[(a + 1) % LEN];
|
self.buffers.l[a] += (1.0 - diss) * self.buffers.l[(a + 1) % LEN];
|
||||||
self.buffer_r[a] += (1.0 - diss) * self.buffer_r[(a + 1) % LEN];
|
self.buffers.r[a] += (1.0 - diss) * self.buffers.r[(a + 1) % LEN];
|
||||||
|
|
||||||
// Loop index after we finish playing a section
|
// Loop index after we finish playing a section
|
||||||
self.idx += 1;
|
self.buffers.idx += 1;
|
||||||
if self.idx >= len {
|
if self.buffers.idx >= len {
|
||||||
self.idx = 0;
|
self.buffers.idx = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If it's not on a repeat section, pass all the audio fully
|
// If it's not on a repeat section, pass all the audio fully
|
||||||
|
|
|
@ -47,6 +47,11 @@ impl<const LEN: usize> Buffers<LEN> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, idx: usize) -> (f32, f32) {
|
||||||
|
let idx = idx % LEN;
|
||||||
|
(self.l[idx], self.r[idx])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.idx = 0;
|
self.idx = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,11 @@ impl<const LEN: usize> DelayLines<LEN> {
|
||||||
self.r.read_slice(r);
|
self.r.read_slice(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_to_buffers(&self, buffers: &mut crate::buffers::Buffers<LEN>) {
|
||||||
|
self.l.read_slice(&mut buffers.l);
|
||||||
|
self.r.read_slice(&mut buffers.r);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_and_advance(&mut self, l: f32, r: f32) {
|
pub fn write_and_advance(&mut self, l: f32, r: f32) {
|
||||||
self.l.write_and_advance(l);
|
self.l.write_and_advance(l);
|
||||||
self.r.write_and_advance(r);
|
self.r.write_and_advance(r);
|
||||||
|
|
Loading…
Reference in a new issue