Compare commits
2 Commits
59b3b9c822
...
e1b2004dcd
Author | SHA1 | Date |
---|---|---|
annieversary | e1b2004dcd | |
annieversary | 2aaad403d1 |
23
README.md
23
README.md
|
@ -64,6 +64,7 @@ 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
|
||||||
- `reverter`: play sound backwards
|
- `reverter`: play sound backwards
|
||||||
|
- `panera`: pan individual notes differently
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
|
@ -264,6 +265,28 @@ this plugin will introduce a delay of `length` samples, since it has to record t
|
||||||
|
|
||||||
in my experience values between 5000 and 8000 tend to work well, but you might want to experiment a bit to see what works for you
|
in my experience values between 5000 and 8000 tend to work well, but you might want to experiment a bit to see what works for you
|
||||||
|
|
||||||
|
### panera
|
||||||
|
|
||||||
|
ever wanted to have each pluck of the guitar have a different pan? do i have something for you then!
|
||||||
|
|
||||||
|
params:
|
||||||
|
- `gain`: pregain added to the detector. doesn't affect output sound
|
||||||
|
- `attack`: attack of the envelope follower
|
||||||
|
- `release`: release of the envelope follower
|
||||||
|
- `gate`: gate level at which a peak is detected
|
||||||
|
|
||||||
|
- `panning mode`: one of {alternating [0, 0.33), sine [0.33, 0.66), random [0.66, 1]}
|
||||||
|
- `lfo freq`: frequency for the sine lfo
|
||||||
|
|
||||||
|
panera consists of a peak detector with a sample and hold lfo tied to the pan. this lfo is sampled each time a new peak comes in, and it's value will be held until the next peak
|
||||||
|
|
||||||
|
there's three panning modes:
|
||||||
|
- alternating: each peak will be hard panned to a different side, alternating between left and right
|
||||||
|
- sine: samples a sine lfo, and uses that as the pan for the rest of the note
|
||||||
|
- random: each note will have a random pan
|
||||||
|
|
||||||
|
the peak detector is still a bit fiddly, so you'll have to tweak the params a bit until it works for the kind of sound you're giving it. the defaults have worked great for me, so i recommend you start from there and change as you see fit
|
||||||
|
|
||||||
## contributing
|
## 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
|
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
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "bistortion"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||||
|
serde = "1.0.126"
|
||||||
|
|
||||||
|
# utils = { path = "../utils" }
|
|
@ -0,0 +1,67 @@
|
||||||
|
#![allow(incomplete_features)]
|
||||||
|
#![feature(generic_associated_types)]
|
||||||
|
|
||||||
|
use baseplug::{Plugin, ProcessContext};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
baseplug::model! {
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct BistortionModel {
|
||||||
|
#[model(min = 0.0, max = 10.0)]
|
||||||
|
#[parameter(name = "a")]
|
||||||
|
a: f32,
|
||||||
|
#[model(min = 0.0, max = 30.0)]
|
||||||
|
#[parameter(name = "b")]
|
||||||
|
b: f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BistortionModel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { a: 1.0, b: 1.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bistortion {
|
||||||
|
frame: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for Bistortion {
|
||||||
|
const NAME: &'static str = "bistortion";
|
||||||
|
const PRODUCT: &'static str = "bistortion";
|
||||||
|
const VENDOR: &'static str = "unnieversal";
|
||||||
|
|
||||||
|
const INPUT_CHANNELS: usize = 2;
|
||||||
|
const OUTPUT_CHANNELS: usize = 2;
|
||||||
|
|
||||||
|
type Model = BistortionModel;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn new(_sample_rate: f32, _model: &BistortionModel) -> Self {
|
||||||
|
Self { frame: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process(&mut self, model: &BistortionModelProcess, ctx: &mut ProcessContext<Self>) {
|
||||||
|
let input = &ctx.inputs[0].buffers;
|
||||||
|
let output = &mut ctx.outputs[0].buffers;
|
||||||
|
|
||||||
|
for i in 0..ctx.nframes {
|
||||||
|
let a = model.a[i];
|
||||||
|
let b = model.b[i];
|
||||||
|
let p = self.frame as f32 / ctx.sample_rate;
|
||||||
|
|
||||||
|
output[0][i] = process(input[0][i], p, a, b);
|
||||||
|
output[1][i] = process(input[1][i], p, a, b);
|
||||||
|
|
||||||
|
self.frame += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.desmos.com/calculator/8gmb283p0v
|
||||||
|
fn process(v: f32, p: f32, a: f32, b: f32) -> f32 {
|
||||||
|
(v * a * (b * v).cos()).tanh()
|
||||||
|
}
|
||||||
|
|
||||||
|
baseplug::vst2!(Bistortion, b"bist");
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "panera"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||||
|
serde = "1.0.126"
|
||||||
|
|
||||||
|
utils = { path = "../utils" }
|
|
@ -0,0 +1,167 @@
|
||||||
|
#![allow(incomplete_features)]
|
||||||
|
#![feature(generic_associated_types)]
|
||||||
|
|
||||||
|
use baseplug::{Plugin, ProcessContext};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use utils::delay::*;
|
||||||
|
use utils::envelope::*;
|
||||||
|
|
||||||
|
const DELAY_LEN: usize = 200;
|
||||||
|
|
||||||
|
baseplug::model! {
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct PaneraModel {
|
||||||
|
// peak detection options
|
||||||
|
#[model(min = 0.0, max = 3.0)]
|
||||||
|
#[parameter(name = "gain")]
|
||||||
|
gain: f32,
|
||||||
|
#[model(min = 0.0, max = 1.0)]
|
||||||
|
#[parameter(name = "attack")]
|
||||||
|
attack: f32,
|
||||||
|
#[model(min = 0.0, max = 1.0)]
|
||||||
|
#[parameter(name = "release")]
|
||||||
|
release: f32,
|
||||||
|
#[model(min = 0.0, max = 1.0)]
|
||||||
|
#[parameter(name = "gate")]
|
||||||
|
gate: f32,
|
||||||
|
|
||||||
|
// options to control the pan oscillation
|
||||||
|
#[model(min = 0.0, max = 1.0)]
|
||||||
|
#[parameter(name = "panning mode")]
|
||||||
|
panning_mode: f32,
|
||||||
|
#[model(min = 0.0, max = 10.0)]
|
||||||
|
#[parameter(name = "lfo freq")]
|
||||||
|
lfo_freq: f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PaneraModel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
gain: 1.8,
|
||||||
|
attack: 0.0,
|
||||||
|
release: 0.27,
|
||||||
|
gate: 0.22,
|
||||||
|
|
||||||
|
panning_mode: 0.0,
|
||||||
|
lfo_freq: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Panera {
|
||||||
|
/// envelope follower so we can detect peaks
|
||||||
|
envelope_follower: EnvelopeFollower,
|
||||||
|
/// whether we are currently on a peak
|
||||||
|
on: bool,
|
||||||
|
/// the current pan position
|
||||||
|
/// goes from 0 (left) to 1 (right)
|
||||||
|
pan: f32,
|
||||||
|
/// delay line is used so we don't change the pan after the peak has started
|
||||||
|
/// since to detect a peak we have to be halfway through it, if we change the pan
|
||||||
|
/// when we detect a peak, this means that it gets changed halfway, which results in a click
|
||||||
|
/// the delay makes it so we can detect a peak before actually playing it, therefore
|
||||||
|
/// allowing us to change the pan before the peak actually starts, and thus no clicks :)
|
||||||
|
delay: DelayLine<DELAY_LEN>,
|
||||||
|
|
||||||
|
lfo_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for Panera {
|
||||||
|
const NAME: &'static str = "panera";
|
||||||
|
const PRODUCT: &'static str = "panera";
|
||||||
|
const VENDOR: &'static str = "unnieversal";
|
||||||
|
|
||||||
|
const INPUT_CHANNELS: usize = 1;
|
||||||
|
const OUTPUT_CHANNELS: usize = 2;
|
||||||
|
|
||||||
|
type Model = PaneraModel;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn new(_sample_rate: f32, _model: &PaneraModel) -> Self {
|
||||||
|
Self {
|
||||||
|
envelope_follower: EnvelopeFollower::new(),
|
||||||
|
on: true,
|
||||||
|
pan: 0.0,
|
||||||
|
delay: DelayLine::<DELAY_LEN>::new(),
|
||||||
|
lfo_idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process(&mut self, model: &PaneraModelProcess, ctx: &mut ProcessContext<Self>) {
|
||||||
|
let input = &ctx.inputs[0].buffers;
|
||||||
|
let output = &mut ctx.outputs[0].buffers;
|
||||||
|
|
||||||
|
for i in 0..ctx.nframes {
|
||||||
|
// set values for env detector
|
||||||
|
self.envelope_follower.set_attack_release(
|
||||||
|
ctx.sample_rate,
|
||||||
|
model.attack[i],
|
||||||
|
model.release[i],
|
||||||
|
);
|
||||||
|
|
||||||
|
// process envelope detector with the non-delayed sample
|
||||||
|
let env = self.envelope_follower.process(input[0][i] * model.gain[i]);
|
||||||
|
|
||||||
|
// detect peak
|
||||||
|
if env > model.gate[i] && !self.on {
|
||||||
|
self.update_pan(
|
||||||
|
model.panning_mode[i].into(),
|
||||||
|
model.lfo_freq[i],
|
||||||
|
ctx.sample_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.on = true;
|
||||||
|
}
|
||||||
|
if env < model.gate[i] && self.on {
|
||||||
|
self.on = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment lfo counter
|
||||||
|
self.lfo_idx += 1;
|
||||||
|
|
||||||
|
// update delay and get value
|
||||||
|
let s = self.delay.write_and_advance_get_last(input[0][i]);
|
||||||
|
|
||||||
|
// square pan law
|
||||||
|
// we play the delayed value
|
||||||
|
output[0][i] = self.pan.sqrt() * s;
|
||||||
|
output[1][i] = (1.0 - self.pan).sqrt() * s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PanningMode {
|
||||||
|
Alternating,
|
||||||
|
Sine,
|
||||||
|
Random,
|
||||||
|
}
|
||||||
|
impl From<f32> for PanningMode {
|
||||||
|
fn from(a: f32) -> Self {
|
||||||
|
if a < 0.33 {
|
||||||
|
Self::Alternating
|
||||||
|
} else if a < 0.66 {
|
||||||
|
Self::Sine
|
||||||
|
} else {
|
||||||
|
Self::Random
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panera {
|
||||||
|
fn update_pan(&mut self, lfo_type: PanningMode, lfo_freq: f32, sample_rate: f32) {
|
||||||
|
self.pan = match lfo_type {
|
||||||
|
PanningMode::Alternating => 1.0 - self.pan,
|
||||||
|
PanningMode::Sine => ((self.lfo_idx as f32 * lfo_freq / sample_rate).sin() + 1.0) / 2.0,
|
||||||
|
PanningMode::Random => {
|
||||||
|
// idk tbh, just something kinda random ig
|
||||||
|
// i just want it to not be predictable
|
||||||
|
(((self.lfo_idx * (self.lfo_idx ^ 1234)) as f32 * lfo_freq).sin() + 1.0) / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseplug::vst2!(Panera, b"pann");
|
|
@ -170,17 +170,13 @@ impl RoboTuna {
|
||||||
self.delay_idx_r += adv_r;
|
self.delay_idx_r += adv_r;
|
||||||
self.true_idx += 1;
|
self.true_idx += 1;
|
||||||
|
|
||||||
// get how close we are to the input idx, so we know if we have to interpolate/jump
|
|
||||||
let l_diff = self.true_idx as f32 - self.delay_idx_l;
|
|
||||||
let r_diff = self.true_idx as f32 - self.delay_idx_r;
|
|
||||||
|
|
||||||
// get the current value
|
// get the current value
|
||||||
let mut l = self.delays.l.floating_index(self.delay_idx_l);
|
let mut l = self.delays.l.floating_index(self.delay_idx_l);
|
||||||
let mut r = self.delays.r.floating_index(self.delay_idx_r);
|
let mut r = self.delays.r.floating_index(self.delay_idx_r);
|
||||||
|
|
||||||
// Interpolation
|
// get how close we are to the input idx, so we know if we have to interpolate/jump
|
||||||
// if we are close to having to jump, we start interpolating with the jump destination
|
let l_diff = self.true_idx as f32 - self.delay_idx_l;
|
||||||
// interpolate when we're one third of the period away from jumping
|
let r_diff = self.true_idx as f32 - self.delay_idx_r;
|
||||||
|
|
||||||
// TODO change to a non-linear interpolation
|
// TODO change to a non-linear interpolation
|
||||||
|
|
||||||
|
@ -189,11 +185,22 @@ impl RoboTuna {
|
||||||
let a = (l_diff - period_l) / (period_l / DIV);
|
let a = (l_diff - period_l) / (period_l / DIV);
|
||||||
l *= a;
|
l *= a;
|
||||||
l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l);
|
l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l);
|
||||||
|
// crossfade
|
||||||
|
// if we are close to having to jump, we start crossfading with the jump destination
|
||||||
|
// crossfade when we're one third of the period away from jumping
|
||||||
|
// when we get close to jumping back
|
||||||
|
if l_diff - period_l < cf_len_l {
|
||||||
|
// cross goes from 1 (when l_diff is at the max) to 0 (when l_diff == period_l)
|
||||||
|
let cross = (l_diff - period_l) / cf_len_l;
|
||||||
|
let (fade_in, fade_out) = ep_crossfade(1.0 - cross);
|
||||||
|
l = fade_out * l + fade_in * self.delays.l.floating_index(self.delay_idx_l - period_l);
|
||||||
}
|
}
|
||||||
if 3.0 * period_l - l_diff < (period_l / DIV) {
|
// when we get close to jumping foward
|
||||||
let a = (3.0 * period_l - l_diff) / (period_l / DIV);
|
if MAX_PERIOD * period_l - l_diff < cf_len_l {
|
||||||
l *= a;
|
// cross goes from 1 (when l_diff is at the min) to 0 (when l_diff == 3.0 * period_l)
|
||||||
l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l);
|
let cross = (MAX_PERIOD * period_l - l_diff) / cf_len_l;
|
||||||
|
let (fade_in, fade_out) = ep_crossfade(1.0 - cross);
|
||||||
|
l = fade_out * l + fade_in * self.delays.l.floating_index(self.delay_idx_l + period_l);
|
||||||
}
|
}
|
||||||
if r_diff - period_r < (period_r / DIV) {
|
if r_diff - period_r < (period_r / DIV) {
|
||||||
let a = (r_diff - period_r) / (period_r / DIV);
|
let a = (r_diff - period_r) / (period_r / DIV);
|
||||||
|
|
|
@ -10,13 +10,7 @@ impl<const LEN: usize> DelayLine<LEN> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_slice(&self, slice: &mut [f32]) {
|
/// write to delay line and advance index
|
||||||
// Copy values in order
|
|
||||||
for i in 0..LEN {
|
|
||||||
slice[i] = self.wrapped_index(self.index + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_and_advance(&mut self, value: f32) {
|
pub fn write_and_advance(&mut self, value: f32) {
|
||||||
self.buffer[self.index] = value;
|
self.buffer[self.index] = value;
|
||||||
|
|
||||||
|
@ -27,6 +21,13 @@ impl<const LEN: usize> DelayLine<LEN> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// write to delay line, advance index, and return the oldest sample
|
||||||
|
pub fn write_and_advance_get_last(&mut self, value: f32) -> f32 {
|
||||||
|
self.write_and_advance(value);
|
||||||
|
|
||||||
|
self.wrapped_index(self.index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the sample at idx after taking modulo LEN
|
/// Returns the sample at idx after taking modulo LEN
|
||||||
pub fn wrapped_index(&self, idx: usize) -> f32 {
|
pub fn wrapped_index(&self, idx: usize) -> f32 {
|
||||||
self.buffer[idx % LEN]
|
self.buffer[idx % LEN]
|
||||||
|
@ -37,9 +38,8 @@ impl<const LEN: usize> DelayLine<LEN> {
|
||||||
let idx = val.trunc() as usize;
|
let idx = val.trunc() as usize;
|
||||||
let frac = val.fract();
|
let frac = val.fract();
|
||||||
|
|
||||||
// TODO uhm idk what this should be, but we don't want an underflow so yeah,
|
|
||||||
let xm1 = if idx == 0 {
|
let xm1 = if idx == 0 {
|
||||||
0.0
|
self.wrapped_index(LEN - 1)
|
||||||
} else {
|
} else {
|
||||||
self.wrapped_index(idx - 1)
|
self.wrapped_index(idx - 1)
|
||||||
};
|
};
|
||||||
|
@ -62,6 +62,13 @@ impl<const LEN: usize> DelayLine<LEN> {
|
||||||
pub fn buffer(&self) -> &[f32; LEN] {
|
pub fn buffer(&self) -> &[f32; LEN] {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_slice(&self, slice: &mut [f32]) {
|
||||||
|
// Copy values in order
|
||||||
|
for i in 0..LEN {
|
||||||
|
slice[i] = self.wrapped_index(self.index + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DelayLines<const LEN: usize> {
|
pub struct DelayLines<const LEN: usize> {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// from https://www.musicdsp.org/en/latest/Analysis/97-envelope-detector.html
|
||||||
|
// with some modifications
|
||||||
|
pub struct EnvelopeFollower {
|
||||||
|
x1: f32,
|
||||||
|
x2: f32,
|
||||||
|
|
||||||
|
ga: f32,
|
||||||
|
gr: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeFollower {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
x1: 0.0,
|
||||||
|
x2: 0.0,
|
||||||
|
ga: 0.0,
|
||||||
|
gr: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// attack and release are in seconds
|
||||||
|
pub fn set_attack_release(&mut self, sample_rate: f32, attack: f32, release: f32) {
|
||||||
|
self.ga = (-1.0 / (sample_rate * attack)).exp();
|
||||||
|
self.gr = (-1.0 / (sample_rate * release)).exp();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process(&mut self, input: f32) -> f32 {
|
||||||
|
let in_abs = input.abs();
|
||||||
|
|
||||||
|
// 2nd order lowpass
|
||||||
|
if self.x1 < in_abs {
|
||||||
|
self.x1 = in_abs + self.ga * (self.x1 - in_abs);
|
||||||
|
self.x2 = self.x1 + self.ga * (self.x2 - self.x1);
|
||||||
|
} else {
|
||||||
|
self.x1 = in_abs + self.gr * (self.x1 - in_abs);
|
||||||
|
self.x2 = self.x1 + self.gr * (self.x2 - self.x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.x2
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod buffers;
|
pub mod buffers;
|
||||||
pub mod delay;
|
pub mod delay;
|
||||||
|
pub mod envelope;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod pitch;
|
pub mod pitch;
|
||||||
pub mod threeband;
|
pub mod threeband;
|
||||||
|
|
3
macos.sh
3
macos.sh
|
@ -1,6 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Make sure we have the arguments we need
|
set -e
|
||||||
|
|
||||||
if [[ -z $1 ]]; then
|
if [[ -z $1 ]]; then
|
||||||
echo "Builds, bundles and copies to /Library/Audio/Plug-Ins/VST a package"
|
echo "Builds, bundles and copies to /Library/Audio/Plug-Ins/VST a package"
|
||||||
echo "Example:"
|
echo "Example:"
|
||||||
|
|
Loading…
Reference in New Issue