Compare commits
4 Commits
512541026b
...
ca16d2e27e
Author | SHA1 | Date |
---|---|---|
annieversary | ca16d2e27e | |
annieversary | 350e6d1bde | |
annieversary | 43605dfa87 | |
annieversary | 62bc461f60 |
65
README.md
65
README.md
|
@ -48,6 +48,7 @@ PRs are welcome for more build instructions
|
|||
|
||||
the following is the current list of plugins
|
||||
|
||||
|
||||
- `basic_gain`: simple gain plugin
|
||||
- `noted`: output midi at regular intervals
|
||||
- `sosten`: granular sustain plugin
|
||||
|
@ -63,6 +64,8 @@ the following is the current list of plugins
|
|||
- `double_reverse_delta_inverter`: idk, a weird distortion
|
||||
- `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
|
||||
|
||||
simple gain plugin, used as a template for starting new projects
|
||||
|
@ -74,26 +77,44 @@ midi retriggerer
|
|||
parameters:
|
||||
- `tempo`: how often to retrigger a note
|
||||
|
||||
it listens for incoming midi, and while a note is on, it triggers it every `tempo` beats. if `tempo` is one, it will trigger once per beat
|
||||
it listens for incoming midi, and while a note is on, it triggers it every `tempo` beats. if `tempo` is one, it will trigger once per beat, if it's `0.5`, it'll play half notes, etc.
|
||||
|
||||
it's useful when you want to play a note super fast (over 1/64 tempo) but don't want to have a section full of tiny midi notes. just add the plugin, set the tempo to how often you want it to play, and add a midi note that lasts however long you want the notes to play for
|
||||
|
||||
### sosten
|
||||
|
||||
sustains a sound by replaying a grain of sound on loop
|
||||
sustains a sound by replaying a grain of sound on loop. small but versatile
|
||||
|
||||
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
|
||||
- `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
|
||||
- `direction`: direction in which the grain is played
|
||||
|
||||
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
|
||||
|
||||
direction is a selector, with three ranges:
|
||||
- `[0, 0.4)`: the grain will be played backwards
|
||||
- `[0.4, 0.6)`: the grain will cycle through forwards and backwards
|
||||
- `[0.6, 1.0]`: the grain will play forwards
|
||||
|
||||
my three main uses are:
|
||||
- sustaining a note, which you can do with `manual/pitch detection = 1`, `direction = 1`, and any amount of dissipation
|
||||
- wavetable-ish kinda thing, by setting `manual = 0` and a really low length, you can play notes of frequency `1 / length`
|
||||
- glitch repetition effect, by setting `manual = 0`, any direction (but usually forward), any dissipation, and a medium length. enable at the end of a bar or smth
|
||||
|
||||
but you can probably use it for more stuff! idk! this are just my suggestions
|
||||
|
||||
### quinoa [WIP]
|
||||
|
||||
nothing implemented yet, but it's supposed to be a granular synthesis plugin that does stuff with an audio file
|
||||
|
||||
since this needs a ui so we can select a file to play, and i still don't know how to do that, it's kinda stuck
|
||||
since this needs a ui so we can select a file to play, and i still don't know how to do that, progress is kinda stuck
|
||||
|
||||
### XLowpass
|
||||
|
||||
|
@ -118,7 +139,13 @@ parameters:
|
|||
- `note_length`: length of each note in beats
|
||||
- `enable`: enables the generation if over 0.5
|
||||
|
||||
since some daws keep the plugins running all the time (whether the song is playing or not), this plugin will generate notes constantly even if the song is paused. since this gets annoying fast, i added an `enable` param. set it to 1 to make the plugin generate notes
|
||||
this plugin is a weird thingy i made to generate midi notes pseudo-randomly. i didn't want it to be actually random, cause that doesn't make many patterns, so what i ended up doing is simulating a ball bouncing around a cube, and sending out a note every time it bounces off a wall. this works well, but a cube only has three dimensions, which is pretty low! so through the magic of computers not giving a shit about physical reality, i made the cube be 16 dimensional, and yeah that's basically it
|
||||
|
||||
the bounces are all perfect and maintain all the energy, so each note ends up playing at a regular interval but they're all playing at different tempos
|
||||
|
||||
works well if you put it in front of a sampler
|
||||
|
||||
the only way to reset the pattern is to reset the plugin. if your daw has a way of doing that, do that, otherwise you're gonna have to remove it and re-add it, or close and open your daw
|
||||
|
||||
### robotuna
|
||||
|
||||
|
@ -137,7 +164,9 @@ parameters:
|
|||
|
||||
original source [here](https://ccrma.stanford.edu/~jatin/ComplexNonlinearities/Hysteresis.html)
|
||||
|
||||
turn the values up for loud and distorted. don't set them all to the max at once though, that doesn't sound as interesting
|
||||
turn the values up for loud and distorted.
|
||||
|
||||
setting them all to the max at once doesn't sound as interesting though, so play around with the values a bit
|
||||
|
||||
### threebandeq
|
||||
|
||||
|
@ -163,7 +192,13 @@ parameters:
|
|||
- `mid width`: width for the mid band
|
||||
- `high width`: width for the high band
|
||||
|
||||
bands work as they do in `threebandeq`. the width parameters control how wide an audio signal should be in the stereo field. `width` of 1 leaves the audio as is, `width` of 0 makes it mono, values between 0 and 1 decrease the stereo width, values over 1 increase it.
|
||||
bands work as they do in `threebandeq`. the width parameters control how wide an audio signal should be in the stereo field
|
||||
|
||||
it behaves as follows depending on value:
|
||||
- `0`: makes audio mono
|
||||
- `(0, 1)`: decreases stereo width
|
||||
- `1`: leaves audio as is
|
||||
- `(1, 3)`: increases stereo width
|
||||
|
||||
### tritu
|
||||
|
||||
|
@ -176,7 +211,7 @@ parameters:
|
|||
|
||||
distortion affects lower volumes more than higher volumes, so if you crank up `drive` a lot, there will be less distortion (this is a good thing for high values of `distortion`!). if you leave `drive` low and set `distortion` to max, your audio will turn to almost white noise
|
||||
|
||||
i like this plugin a lot
|
||||
i like this plugin a lot :)
|
||||
|
||||
### threebandfolding
|
||||
|
||||
|
@ -189,7 +224,7 @@ parameters:
|
|||
- `mid folding freq`: folding frequency for the mid band
|
||||
- `high folding freq`: folding frequency for the high band
|
||||
|
||||
bands work as they do in `threebandeq`. the `folding freq` parameters control the frequency of the wavefolding. higher is more distortion. use values over 40 if you want to hear white noise
|
||||
bands work as they do in `threebandeq`. the `folding freq` parameters control the frequency of the wavefolder. higher is more distortion. use values over 40 if you want to have basically white noise
|
||||
|
||||
### double-reverse delta inverter
|
||||
|
||||
|
@ -198,7 +233,9 @@ weird kinda distortion that makes loud things quiet and makes quiet things loud
|
|||
parameters:
|
||||
- `dry/wet`: dry/wet control
|
||||
|
||||
yeah there's not many params in this one, the implementation is pretty straightforward. it does weird things: a square wave of amplitude 1 will just be completely eliminated, while sine waves are distorted
|
||||
yeah there's not many params in this one, the implementation is pretty straightforward
|
||||
|
||||
it does weird things: a square wave of amplitude 1 will just be completely eliminated, while sine waves are distorted. since it can't be really controlled, use the `dry/wet` to lower the effect if you don't want to go overboard
|
||||
|
||||
the effect is reversible (it's an [involution](https://en.wikipedia.org/wiki/Involution_(mathematics))), so if you add this plugin twice in a row you get the original signal back. this means you can apply drdi to your audio, apply some other effect, and apply drdi again to get some fun stuff
|
||||
|
||||
|
@ -209,10 +246,12 @@ analyzes pitch of incoming audio and sends out the corresponding midi note
|
|||
params:
|
||||
- `passthrough`: how much of the original audio to be let through
|
||||
|
||||
latency should be close to 1024 samples, which is around 20ms i think. the pitch detection is not excellent and it's monophonic, so if you have a noisy input or a bunch of notes at the same time it's anyone's guess what the midi output will be. there's also no pitch bending or anything fancy like that, so it'll jump around notes even if the input has portamento.
|
||||
latency should be close to 1024 samples, which is around 20ms at 48k i think. the pitch detection is not excellent and it's monophonic, so if you have a noisy input or a bunch of notes at the same time, it's anyone's guess what the detected note will be. there's also no pitch bending or anything fancy like that, so it'll jump around notes even if the input has portamento
|
||||
|
||||
aside from that, it works well enough on my quick tests
|
||||
|
||||
you can use it to play synths using other instruments, like a guitar, or by whistling! it's quite fun
|
||||
|
||||
## contributing
|
||||
|
||||
issues and prs are welcome, but please open an issue before making any big pr, i don't wanna have to reject a pr where you have put a lot of effort on. if you are fine with that, ig go ahead i'm not your mum
|
||||
|
|
|
@ -4,44 +4,62 @@
|
|||
use baseplug::{Plugin, ProcessContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use utils::buffers::*;
|
||||
use utils::delay::*;
|
||||
use utils::pitch::*;
|
||||
|
||||
// If you change this remember to change the max on the model
|
||||
const LEN: usize = 48000;
|
||||
const PITCH_LEN: usize = 2 << 9;
|
||||
|
||||
baseplug::model! {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SostenModel {
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "enable")]
|
||||
enable: f32,
|
||||
#[model(min = 10.0, max = 48000.0)]
|
||||
#[parameter(name = "length")]
|
||||
length: f32,
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "mix")]
|
||||
mix: f32,
|
||||
#[parameter(name = "manual/pitch detection")]
|
||||
manual: f32,
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "enable")]
|
||||
enable: f32,
|
||||
#[parameter(name = "dissipation")]
|
||||
dissipation: f32,
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "direction")]
|
||||
direction: f32,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SostenModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
length: 1000.0,
|
||||
mix: 1.0,
|
||||
enable: 0.0,
|
||||
length: 1000.0,
|
||||
manual: 0.0,
|
||||
dissipation: 1.0,
|
||||
direction: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Sosten {
|
||||
delay_l: DelayLine<LEN>,
|
||||
delay_r: DelayLine<LEN>,
|
||||
buffer_l: [f32; LEN],
|
||||
buffer_r: [f32; LEN],
|
||||
delay: DelayLines<LEN>,
|
||||
buffers: Buffers<LEN>,
|
||||
|
||||
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>,
|
||||
|
||||
/// Keeps track of whether we are going forward while in cycle mode
|
||||
forwards_in_cycle_mode: bool,
|
||||
}
|
||||
|
||||
impl Plugin for Sosten {
|
||||
|
@ -57,13 +75,16 @@ impl Plugin for Sosten {
|
|||
#[inline]
|
||||
fn new(_sample_rate: f32, _model: &SostenModel) -> Self {
|
||||
Self {
|
||||
delay_l: DelayLine::<LEN>::new(),
|
||||
delay_r: DelayLine::<LEN>::new(),
|
||||
buffer_l: [0.; LEN],
|
||||
buffer_r: [0.; LEN],
|
||||
delay: DelayLines::<LEN>::new(),
|
||||
buffers: Buffers::new(),
|
||||
|
||||
playing: false,
|
||||
idx: 0,
|
||||
|
||||
detector_thread: pitch_detection::PitchDetectorThread::<PITCH_LEN>::new(),
|
||||
used_period: None,
|
||||
period: None,
|
||||
|
||||
forwards_in_cycle_mode: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,19 +94,34 @@ impl Plugin for Sosten {
|
|||
let output = &mut ctx.outputs[0].buffers;
|
||||
|
||||
for i in 0..ctx.nframes {
|
||||
// Update delays
|
||||
self.delay_l.write_and_advance(input[0][i]);
|
||||
self.delay_r.write_and_advance(input[1][i]);
|
||||
// update delays
|
||||
self.delay.write_and_advance(input[0][i], 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`
|
||||
if model.enable[i] >= 0.5 {
|
||||
// If it wasn't playing before this, reload buffer
|
||||
if !self.playing {
|
||||
self.delay_l.read_slice(&mut self.buffer_l);
|
||||
self.delay_r.read_slice(&mut self.buffer_r);
|
||||
self.idx = 0;
|
||||
self.delay.read_to_buffers(&mut self.buffers);
|
||||
self.buffers.reset();
|
||||
|
||||
self.playing = true;
|
||||
self.used_period = self.period;
|
||||
}
|
||||
} else {
|
||||
self.playing = false;
|
||||
|
@ -94,24 +130,63 @@ impl Plugin for Sosten {
|
|||
// Play the repeating part
|
||||
if self.playing {
|
||||
// Length of section to play
|
||||
let len = model.length[i].trunc() as usize;
|
||||
|
||||
// Pass through input
|
||||
let mix_inv = 1. - model.mix[i];
|
||||
output[0][i] = mix_inv * input[0][i];
|
||||
output[1][i] = mix_inv * input[1][i];
|
||||
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
|
||||
let idx = self.idx.min(len - 1);
|
||||
let idx = self.buffers.idx.min(len - 1);
|
||||
|
||||
// Play from Buffer
|
||||
output[0][i] += model.mix[i] * self.buffer_l[(LEN - len) + idx];
|
||||
output[1][i] += model.mix[i] * self.buffer_r[(LEN - len) + idx];
|
||||
let (l, r) = self.buffers.read((LEN - len) + idx);
|
||||
|
||||
output[0][i] = l;
|
||||
output[1][i] = r;
|
||||
|
||||
// dissipates the audio in the buffer, idk
|
||||
// it adds a bit of the next sample to this one, which smooths it out
|
||||
let diss = model.dissipation[i];
|
||||
let a = (LEN - len) + idx;
|
||||
self.buffers.l[a] *= diss;
|
||||
self.buffers.r[a] *= diss;
|
||||
self.buffers.l[a] += (1.0 - diss) * self.buffers.l[(a + 1) % LEN];
|
||||
self.buffers.r[a] += (1.0 - diss) * self.buffers.r[(a + 1) % LEN];
|
||||
|
||||
// Loop index after we finish playing a section
|
||||
self.idx += 1;
|
||||
if self.idx >= len {
|
||||
self.idx = 0;
|
||||
if model.direction[i] > 0.6 {
|
||||
// we are going forwards, so increase the idx
|
||||
self.buffers.idx += 1;
|
||||
// and loop at the end
|
||||
if self.buffers.idx >= len {
|
||||
self.buffers.idx = 0;
|
||||
}
|
||||
} else if model.direction[i] > 0.4 {
|
||||
// we're in cycle mode
|
||||
|
||||
// alternate directions at the end
|
||||
if self.buffers.idx == 0 {
|
||||
self.forwards_in_cycle_mode = true;
|
||||
}
|
||||
if self.buffers.idx >= len {
|
||||
self.forwards_in_cycle_mode = false;
|
||||
}
|
||||
|
||||
// increase/decrease index
|
||||
if self.forwards_in_cycle_mode {
|
||||
self.buffers.idx += 1;
|
||||
} else {
|
||||
self.buffers.idx -= 1;
|
||||
}
|
||||
} else {
|
||||
// we are going backwards, so loop first
|
||||
// this ensures we don't do 0 - 1
|
||||
if self.buffers.idx == 0 {
|
||||
self.buffers.idx = len + 1;
|
||||
}
|
||||
// then decrease index
|
||||
self.buffers.idx -= 1;
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
self.idx = 0;
|
||||
}
|
||||
|
|
|
@ -81,6 +81,11 @@ impl<const LEN: usize> DelayLines<LEN> {
|
|||
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) {
|
||||
self.l.write_and_advance(l);
|
||||
self.r.write_and_advance(r);
|
||||
|
|
Loading…
Reference in New Issue