Compare commits

..

5 Commits

38 changed files with 3549 additions and 12 deletions

View File

@ -65,6 +65,7 @@ the following is the current list of plugins
- `transmute_pitch`: pitch to midi converter
- `reverter`: play sound backwards
- `panera`: pan individual notes differently
- `velociter`: random velocity setter
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
@ -287,6 +288,16 @@ there's three panning modes:
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
### velociter
velociter sets random velocities to midi notes
params:
- `min_vel`: minimum velocity
- `max_vel`: maximum velocity
it'll set the velocity of every incoming midi note to be in the range `[min_vel, max_vel)`
## 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
crates/baseplug/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
/baseplug-derive/target
Cargo.lock
.vscode

View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at git@wrl.lhiaudio.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -0,0 +1,38 @@
[package]
name = "baseplug"
version = "0.1.0"
authors = ["William Light <git@wrl.lhiaudio.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
[dependencies]
num-traits = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
vst2-sys = "0.2.0"
raw-window-handle = "0.3"
[dev-dependencies.packed_simd]
version = "0.3"
package = "packed_simd_2"
[dependencies.baseplug-derive]
path = "baseplug-derive"
[[example]]
name = "gain"
crate-type = ["cdylib"]
[[example]]
name = "svf"
crate-type = ["cdylib"]
[[example]]
name = "midi_sine"
crate-type = ["cdylib"]
[[example]]
name = "midi_out_metronome"
crate-type = ["cdylib"]

View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

36
crates/baseplug/README Normal file
View File

@ -0,0 +1,36 @@
__ __
| |--.---.-.-----.-----.-----.| |.--.--.-----.
| _ | _ |__ --| -__| _ || || | | _ |
|_____|___._|_____|_____| __||__||_____|___ |
|__| |_____|
baseplug is a high-level model/view/controller-ish audio plugin framework.
nightly-only right now, because of GATs and min_specialization.
baseplug is still largely prototypal, but is maturing quickly. i highly
encourage looking at the examples and building experiments with it. though
there will be API breakage in the future, i do not see a significant
divergence from the existing structures.
with that said, baseplug is not currently in a place where you should *ship*
any plugins built with it. as the baseplug APIs change, there is a high
likelihood that your users' sessions will break, presets won't load
correctly, and automation will be incorrect.
the primary goal for the 0.1 milestone is resolving these issues of
forward-compatibility, and at that juncture we will lift the above advisement
against shipping plugins.
i'm making this public in its pre-pre-alpha state to get feedback from folks.
please file issues if things act weird or something's unclear.
do note that, though i have extensive experience in audio dev, i am largely a
neophyte when it comes to advanced rust usage, proper code style and best
practices. especially when it comes to error handling in procmacros.
best way to get in touch is to join the rust audio discord:
https://discord.gg/8rPCp9Q
-w

View File

@ -0,0 +1,15 @@
[package]
name = "baseplug-derive"
version = "0.1.0"
authors = ["William Light <git@wrl.lhiaudio.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["default", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"

View File

@ -0,0 +1,15 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input};
mod model;
#[proc_macro]
pub fn model(input: TokenStream) -> TokenStream {
model::derive(parse_macro_input!(input))
.into()
}
#[proc_macro_derive(Parameters, attributes(model, parameter, unsmoothed))]
pub fn derive_parameters(_input: TokenStream) -> TokenStream {
TokenStream::new()
}

View File

@ -0,0 +1,559 @@
use std::str::FromStr;
use proc_macro2::*;
use syn::*;
use quote::*;
enum WrappingType {
Smooth,
Declick
}
impl WrappingType {
fn for_type(ty: &Path) -> Self {
if ty.is_ident("f32") {
Self::Smooth
} else {
Self::Declick
}
}
fn as_token_stream(&self) -> TokenStream {
use WrappingType::*;
match self {
Smooth => quote!(::baseplug::Smooth),
Declick => quote!(::baseplug::Declick)
}
}
}
#[derive(Debug)]
struct ModelBounds {
min: f32,
max: f32
}
impl Default for ModelBounds {
fn default() -> Self {
Self {
min: 0.0,
max: 1.0,
}
}
}
struct ParameterInfo {
name: String,
short_name: Option<String>,
label: Option<String>,
unit: Option<String>,
gradient: Option<String>,
dsp_notify: Option<String>
}
struct FieldInfo<'a> {
vis: &'a Visibility,
ident: &'a Ident,
ty: &'a Type,
wrapping: Option<WrappingType>,
bounds: ModelBounds,
smooth_ms: f32,
parameter_info: Option<ParameterInfo>
}
impl<'a> FieldInfo<'a> {
fn from_field(f: &'a Field) -> Self {
// FIXME: pub?
let vis = &f.vis;
let ident = f.ident.as_ref().unwrap();
let ty = &f.ty;
let mut info = FieldInfo {
vis,
ident,
ty,
wrapping: match &f.ty {
Type::Path(ref p) => Some(WrappingType::for_type(&p.path)),
_ => None
},
bounds: ModelBounds::default(),
smooth_ms: 5.0f32,
parameter_info: None
};
for attr in f.attrs.iter() {
let meta = attr.parse_meta();
let (ident, nested) = match meta {
Ok(Meta::List(ref list)) => {
(list.path.get_ident().unwrap(), &list.nested)
},
Ok(Meta::Path(ref path)) => {
if path.is_ident("unsmoothed") {
info.wrapping = None;
}
continue
},
_ => continue,
};
match &*ident.to_string() {
"model" => info.populate_model_attrs(nested),
"parameter" => info.populate_parameter_attrs(nested),
ident => panic!("unexpected attribute {}", ident)
}
}
info
}
fn populate_parameter_attrs(&mut self,
nested: &syn::punctuated::Punctuated<syn::NestedMeta, syn::token::Comma>) {
if self.parameter_info.is_some() {
panic!("duplicate parameter info for model field");
}
let mut name = None;
let mut short_name = None;
let mut label = None;
let mut unit = None;
let mut gradient = None;
let mut dsp_notify = None;
nested.iter()
.filter_map(|attr| {
match attr {
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
let lit = match lit {
Lit::Str(s) => s.value(),
_ => return None
};
path.get_ident()
.map(|ident| (ident, lit))
},
_ => None
}
})
.for_each(|(ident, lit)| {
match (&*ident.to_string(), lit) {
("name", s) => name = Some(s),
("short_name", s) => short_name = Some(s),
("label", s) => label = Some(s),
("unit", s) => unit = Some(s),
("gradient", s) => gradient = Some(s),
("dsp_notify", s) => dsp_notify = Some(s),
(ident, _) => panic!("unexpected attribute \"{}\"", ident)
}
});
let name = name.expect("\"name\" is a required parameter field");
self.parameter_info = Some(ParameterInfo {
name,
short_name,
label,
unit,
gradient,
dsp_notify
});
}
fn populate_model_attrs(&mut self,
nested: &syn::punctuated::Punctuated<syn::NestedMeta, syn::token::Comma>) {
nested.iter()
.filter_map(|attr| {
match attr {
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) =>
path.get_ident()
.map(|ident| (ident, lit)),
_ => None
}
})
.for_each(|(ident, lit)| {
match (&*ident.to_string(), lit) {
("min", Lit::Float(f)) => self.bounds.min = f.base10_parse().unwrap(),
("max", Lit::Float(f)) => self.bounds.max = f.base10_parse().unwrap(),
("smooth_ms", Lit::Float(f)) => self.smooth_ms = f.base10_parse().unwrap(),
_ => ()
}
});
}
fn parameter_repr(&self, model: &Ident) -> Option<TokenStream> {
let param = match self.parameter_info {
Some(ref p) => p,
None => return None
};
let pty = quote!(::baseplug::Param<P, #model>);
let ident = &self.ident;
let name = &param.name;
let short_name = param.short_name.as_ref()
.map_or_else(|| quote!(None), |sn| quote!(Some(#sn)));
let label = param.label.as_ref()
.map_or_else(|| quote!(""), |l| quote!(#l));
let dsp_notify = param.dsp_notify.as_ref()
.map_or_else(|| quote!(None), |dn| {
let dn = TokenStream::from_str(dn).unwrap();
quote!(Some(#dn))
});
let unit = param.unit.as_ref()
.map_or_else(
|| quote!(Generic),
|u| TokenStream::from_str(u).unwrap());
let param_type = {
let min = self.bounds.min;
let max = self.bounds.max;
let gradient = param.gradient.as_ref()
.map_or_else(
|| quote!(Linear),
|l| TokenStream::from_str(l).unwrap());
quote!(
::baseplug::parameter::Type::Numeric {
min: #min,
max: #max,
gradient: ::baseplug::parameter::Gradient::#gradient
}
)
};
let model_get = match self.wrapping {
None => quote!(model.#ident),
_ => quote!(model.#ident.dest())
};
let display_cb = match param.unit.as_ref().map(|x| x.as_str()) {
Some("Decibels") => quote!(
|param: &#pty, model: &#model, w: &mut ::std::io::Write| ->
::std::io::Result<()> {
let val = #model_get;
if val <= 0.00003162278 {
write!(w, "-inf")
} else {
write!(w, "{:.1}", ::baseplug::util::coeff_to_db(val))
}
}
),
_ => quote!(
|param: &#pty, model: &#model, w: &mut ::std::io::Write| ->
::std::io::Result<()> {
write!(w, "{}", #model_get)
}
),
};
let set_cb = match self.wrapping {
None => quote!(
|param: &#pty, model: &mut #model, val: f32| {
model.#ident = val.xlate_from(param);
}
),
_ => quote!(
|param: &#pty, model: &mut #model, val: f32| {
model.#ident.set(val.xlate_from(param))
}
)
};
let get_cb = quote!(
|param: &#pty, model: &#model| -> f32 {
#model_get.xlate_out(param)
}
);
Some(quote!(
::baseplug::Param {
name: #name,
short_name: #short_name,
unit: ::baseplug::parameter::Unit::#unit,
param_type: #param_type,
format: ::baseplug::parameter::Format {
display_cb: #display_cb,
label: #label
},
dsp_notify: #dsp_notify,
set_cb: #set_cb,
get_cb: #get_cb
}
))
}
}
pub(crate) fn derive(input: DeriveInput) -> TokenStream {
let attrs = &input.attrs;
let model_vis = &input.vis;
let model_name = &input.ident;
let fields = match input.data {
Data::Struct(DataStruct {
fields: Fields::Named(ref n), ..
}) => &n.named,
_ => panic!()
};
let fields_base: Vec<_> = fields.iter()
.map(FieldInfo::from_field)
.collect();
let model_fields = fields_base.iter()
.map(|FieldInfo { vis, ident, ty, .. }| {
quote!(#vis #ident: #ty)
});
let smoothed_fields = fields_base.iter()
.map(|FieldInfo { vis, ident, wrapping, ty, .. }| {
match wrapping {
Some(wrap_type) => {
let smoothed_type = wrap_type.as_token_stream();
quote!(#vis #ident: #smoothed_type<#ty>)
},
None => quote!(#vis #ident: #ty)
}
});
let proc_fields = fields_base.iter()
.map(|FieldInfo { vis, ident, wrapping, ty, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(#vis #ident:
::baseplug::SmoothOutput<'proc, #ty>),
Some(WrappingType::Declick) =>
quote!(#vis #ident:
::baseplug::DeclickOutput<'proc, #ty>),
None => quote!(#vis #ident: &'proc #ty)
}
});
let get_process_fields = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(#ident: {
let out = self.#ident.output();
::baseplug::SmoothOutput {
values: &out.values[..nframes],
status: out.status
}
}),
Some(WrappingType::Declick) =>
quote!(#ident: {
let out = self.#ident.output();
::baseplug::DeclickOutput {
from: out.from,
to: out.to,
fade: &out.fade[..nframes],
status: out.status
}
}),
None => quote!(#ident: &self.#ident)
}
});
let current_value_fields = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(#ident: {
let out = self.#ident.current_value();
::baseplug::SmoothOutput {
values: out.values,
status: out.status
}
}),
Some(WrappingType::Declick) =>
quote!(#ident: {
let out = self.#ident.current_value();
::baseplug::DeclickOutput {
from: out.from,
to: out.to,
fade: out.fade,
status: out.status
}
}),
None => quote!(#ident: &self.#ident)
}
});
let set_statements = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(self.#ident.set(from.#ident)),
Some(WrappingType::Declick) =>
quote!(self.#ident.set(from.#ident.clone())),
None => quote!(self.#ident = from.#ident)
}
});
let from_model_fields = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(#ident: ::baseplug::Smooth::new(model.#ident)),
Some(WrappingType::Declick) =>
quote!(#ident: ::baseplug::Declick::new(model.#ident)),
None => quote!(#ident: model.#ident)
}
});
let reset_statements = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) =>
quote!(self.#ident.reset(from.#ident)),
Some(WrappingType::Declick) =>
quote!(self.#ident.reset(from.#ident.clone())),
None => quote!(self.#ident = from.#ident)
}
});
let process_statements = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
wrapping.as_ref().map(|_|
quote!(self.#ident.process(nframes)))
});
let set_sample_rate_statements = fields_base.iter()
.map(|FieldInfo { ident, wrapping, smooth_ms, .. }| {
wrapping.as_ref().map(|_|
quote!(self.#ident.set_speed_ms(sample_rate, #smooth_ms)))
});
let as_model_fields = fields_base.iter()
.map(|FieldInfo { ident, wrapping, .. }| {
match wrapping {
Some(WrappingType::Smooth) => quote!(#ident: self.#ident.dest()),
Some(WrappingType::Declick) =>
quote!(#ident: self.#ident.dest().clone()),
None => quote!(#ident: self.#ident)
}
});
let smoothed_ident = format_ident!("{}Smooth", model_name);
let proc_ident = format_ident!("{}Process", model_name);
let impl_params = format_ident!("_IMPL_PARAMETERS_FOR_{}", model_name);
let parameters = fields_base.iter()
.filter_map(|field: &FieldInfo|
field.parameter_repr(&smoothed_ident));
quote!(
#( #attrs )*
#model_vis struct #model_name {
#( #model_fields ),*
}
#[doc(hidden)]
#model_vis struct #smoothed_ident {
#( #smoothed_fields ),*
}
#model_vis struct #proc_ident<'proc> {
#( #proc_fields ),*
}
#[doc(hidden)]
impl<P: ::baseplug::Plugin> ::baseplug::Model<P> for #model_name {
type Smooth = #smoothed_ident;
}
#[doc(hidden)]
impl<P: ::baseplug::Plugin> ::baseplug::SmoothModel<P, #model_name> for #smoothed_ident {
type Process<'proc> = #proc_ident<'proc>;
fn from_model(model: #model_name) -> Self {
Self {
#( #from_model_fields ),*
}
}
fn as_model(&self) -> #model_name {
#model_name {
#( #as_model_fields ),*
}
}
fn set(&mut self, from: &#model_name) {
#( #set_statements ;)*
}
fn reset(&mut self, from: &#model_name) {
#( #reset_statements ;)*
}
fn set_sample_rate(&mut self, sample_rate: f32) {
#( #set_sample_rate_statements ;)*
}
fn current_value<'proc>(&'proc mut self) -> Self::Process<'proc> {
#proc_ident {
#( #current_value_fields ),*
}
}
fn process<'proc>(&'proc mut self, nframes: usize) -> Self::Process<'proc> {
#( #process_statements ;)*
#proc_ident {
#( #get_process_fields ),*
}
}
}
#[doc(hidden)]
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const #impl_params: () = {
use ::baseplug::parameter::{
Translatable,
TranslateFrom
};
impl<P: ::baseplug::Plugin> ::baseplug::Parameters<P, #smoothed_ident> for #smoothed_ident {
const PARAMS: &'static [&'static ::baseplug::Param<P, #smoothed_ident>] = &[
#( & #parameters ),*
];
}
};
)
}

View File

@ -0,0 +1,38 @@
# VST2
## FFI
- [ ] `dispatch(opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize` - Dispatch an event with an opcode.
- [ ] `Get VST API Version` - Return the VST API version.
- [ ] `Shutdown` - Shut down the plugin.
- [ ] `SetSampleRate` - Set the sample rate to `opt`.
- [ ] `StateChanged` - (Is this a call to reset the plugin I assume?)
- [ ] `GetParameterName` - Store the name of the parameter at `index` into `ptr. Return 0 for success.
- [ ] `GetParameterLabel` - Store the label of the parameter at `index` into `ptr. Return 0 for success.
- [ ] `GetParameterDisplay` - (Not sure what this does)
- [ ] `CanBeAutomated` - (Not sure what this does)
- [ ] `GetEffectName` - Store the effect name into `ptr`. Return 1 for success.
- [ ] `GetProductName` - Store the product name into `ptr`. Return 1 for success.
- [ ] `GetVendorName` - Store the vendor name into `ptr`. Return 1 for success.
- [ ] `GetCurrentPresetName` - (Incomplete I assume?)
- [ ] `ProcessEvent` - (Not sure what this does)
- [ ] `GetData` - (Not sure what this does)
- [ ] `SetData` - (Not sure what this does)
- [ ] `EditorGetRect` - Store initial plugin window size into `ptr`. The host may call this before opening the plugin editor window. Returning the correct size based on DPI scaling can be acheived by first a VST extension, second from a user-supplied config-file, and third from guessing the DPI scaling of the system.
- [ ] `EditorOpen` - Open the editor window. (Is `ptr` a handle to the window?)
- [ ] `EditorClose` - Close the editor window.
- [ ] `UnhandledOpCode` - Print the unhandled opcode.
- [ ] `get_parameter(index: i32) -> f32` - Retreive the current value of the parameter at `index`.
- [ ] `set_parameter(index: i32, val: f32)` - Set the value of the parameter at `index`.
- [ ] `get_musical_time() -> MusicalTime { bmp: f64, beat: f64 }` - Retreive musical time information.
- [ ] `process_replacing(in_buffers: *const *const f32, out_buffers: *mut *mut f32, nframes: i32)` - Process buffers.
# VST3
## FFI
- [ ] (commands)
# AU
## FFI
- [ ] (commands)
# LV2
## FFI
- [ ] (commands)

View File

@ -0,0 +1,62 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
use serde::{Serialize, Deserialize};
use baseplug::{
ProcessContext,
Plugin,
};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct GainModel {
#[model(min = -90.0, max = 3.0)]
#[parameter(name = "gain", unit = "Decibels",
gradient = "Power(0.15)")]
gain: f32
}
}
impl Default for GainModel {
fn default() -> Self {
Self {
// "gain" is converted from dB to coefficient in the parameter handling code,
// so in the model here it's a coeff.
// -0dB == 1.0
gain: 1.0
}
}
}
struct Gain;
impl Plugin for Gain {
const NAME: &'static str = "basic gain plug";
const PRODUCT: &'static str = "basic gain plug";
const VENDOR: &'static str = "spicy plugins & co";
const INPUT_CHANNELS: usize = 2;
const OUTPUT_CHANNELS: usize = 2;
type Model = GainModel;
#[inline]
fn new(_sample_rate: f32, _model: &GainModel) -> Self {
Self
}
#[inline]
fn process(&mut self, model: &GainModelProcess, ctx: &mut ProcessContext<Self>) {
let input = &ctx.inputs[0].buffers;
let output = &mut ctx.outputs[0].buffers;
for i in 0..ctx.nframes {
output[0][i] = input[0][i] * model.gain[i];
output[1][i] = input[1][i] * model.gain[i];
}
}
}
baseplug::vst2!(Gain, b"tAnE");

View File

@ -0,0 +1,105 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
#![feature(min_specialization)]
use serde::{Deserialize, Serialize};
use baseplug::{event::Data, Event, Plugin, ProcessContext};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct MidiOutMetronomeModel {
#[model(min = 0.5, max = 2.0)]
#[parameter(name = "len")]
len: f32,
}
}
impl Default for MidiOutMetronomeModel {
fn default() -> Self {
Self { len: 1.0 }
}
}
struct MidiOutMetronome {
note_on: bool,
on_ct: u64,
frame_ct: u64,
}
impl Plugin for MidiOutMetronome {
const NAME: &'static str = "midi out metronome plug";
const PRODUCT: &'static str = "midi out metronome plug";
const VENDOR: &'static str = "spicy plugins & co";
const INPUT_CHANNELS: usize = 2;
const OUTPUT_CHANNELS: usize = 2;
type Model = MidiOutMetronomeModel;
fn new(_sample_rate: f32, _model: &Self::Model) -> Self {
Self {
note_on: false,
on_ct: 0,
frame_ct: 0,
}
}
fn process<'proc>(&mut self, model: &MidiOutMetronomeModelProcess,
ctx: &'proc mut ProcessContext<Self>)
{
let output = &mut ctx.outputs[0].buffers;
let enqueue_midi = &mut ctx.enqueue_event;
// get the current beat and tempo
let curr_bpm = ctx.musical_time.bpm;
let is_playing = ctx.musical_time.is_playing;
for i in 0..ctx.nframes {
// write silence
output[0][i] = 0.0;
output[1][i] = 0.0;
// calc
let beat_in_ms = 60_000.0 / curr_bpm;
let beat_in_samples = beat_in_ms * ctx.sample_rate as f64 / 1000.0;
let sixth_in_samples = (beat_in_samples / 4.0) * model.len[i] as f64;
let beat_in_samples = beat_in_samples.round() as u64;
let sixth_in_samples = sixth_in_samples.round() as u64;
if is_playing && self.frame_ct % beat_in_samples == 0 {
// send a note on (C2)
let note_on = Event::<MidiOutMetronome> {
frame: i,
data: Data::Midi([144, 36, 120]),
};
enqueue_midi(note_on);
self.note_on = true;
self.on_ct = 0;
}
if is_playing && self.note_on && self.on_ct == sixth_in_samples {
// send a note off (C2)
let note_off = Event::<MidiOutMetronome> {
frame: i,
data: Data::Midi([128, 36, 0]),
};
enqueue_midi(note_off);
self.note_on = false;
}
if is_playing {
if self.note_on {
self.on_ct += 1;
}
self.frame_ct += 1;
} else {
self.frame_ct = 0;
}
}
}
}
baseplug::vst2!(MidiOutMetronome, b"~MM~");

View File

@ -0,0 +1,155 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
#![feature(min_specialization)]
use std::f32::consts::PI;
use serde::{Serialize, Deserialize};
use baseplug::{
ProcessContext,
Plugin,
MidiReceiver,
util::db_to_coeff
};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct MidiSineModel {
#[model(min = -90.0, max = 3.0)]
#[parameter(name = "gain", unit = "Decibels",
gradient = "Power(0.15)")]
gain: f32,
#[model(min = 0.05, max = 0.95)]
#[parameter(name = "phase distortion")]
pd: f32,
#[model(min = 220.0, max = 880.0)]
#[parameter(name = "a4 tuning", gradient = "Exponential")]
a4: f32
}
}
impl Default for MidiSineModel {
fn default() -> Self {
Self {
gain: db_to_coeff(-3.0),
pd: 0.5,
a4: 440.0
}
}
}
struct Oscillator {
phase: f64,
step: f64
}
impl Oscillator {
#[inline]
fn new() -> Self {
Self {
// cheeky little hack to keep cosine output from jumping to +1.0 when adding the plugin
// to the host ;>
phase: 0.25,
step: 0.0
}
}
#[inline]
fn set_frequency(&mut self, frequency: f64, sample_rate: f64) {
self.step = frequency / sample_rate;
}
#[inline]
fn tick(&mut self) {
self.phase += self.step;
// usually cheaper than modulo
while self.phase > 1.0 {
self.phase -= 1.0;
}
}
#[inline]
fn pd_phase(&self, d: f32) -> f32 {
let mut phase = self.phase as f32;
if phase < d {
phase /= d;
} else {
phase = 1.0 + ((phase - d) / (1.0 - d));
}
phase * 0.5
}
}
struct MidiSine {
osc: Oscillator,
sample_rate: f32,
freq_ratio: f32,
}
impl Plugin for MidiSine {
const NAME: &'static str = "midi sine plug";
const PRODUCT: &'static str = "midi sine plug";
const VENDOR: &'static str = "spicy plugins & co";
const INPUT_CHANNELS: usize = 0;
const OUTPUT_CHANNELS: usize = 2;
type Model = MidiSineModel;
#[inline]
fn new(sample_rate: f32, _model: &MidiSineModel) -> Self {
Self {
osc: Oscillator::new(),
sample_rate,
freq_ratio: 0.0
}
}
#[inline]
fn process(&mut self, model: &MidiSineModelProcess, ctx: &mut ProcessContext<Self>) {
let output = &mut ctx.outputs[0].buffers;
for i in 0..ctx.nframes {
if model.a4.is_smoothing() {
self.osc.set_frequency((self.freq_ratio * model.a4[i]) as f64, self.sample_rate as f64);
}
let wave = {
let phase = self.osc.pd_phase(model.pd[i]);
(phase * 2.0 * PI).cos()
};
self.osc.tick();
output[0][i] = wave * model.gain[i];
output[1][i] = wave * model.gain[i];
}
}
}
impl MidiReceiver for MidiSine {
fn midi_input(&mut self, model: &MidiSineModelProcess, data: [u8; 3]) {
match data[0] {
// note on
0x90 => {
let ratio = ((data[1] as f32 - 69.0) / 12.0).exp2();
self.freq_ratio = ratio;
let freq = ratio * model.a4[0];
self.osc.set_frequency(freq as f64, self.sample_rate as f64);
},
_ => ()
}
}
}
baseplug::vst2!(MidiSine, b"~Ss~");

View File

@ -0,0 +1,84 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
use serde::{Serialize, Deserialize};
use packed_simd::f32x4;
mod svf_simper;
use svf_simper::SVFSimper;
use baseplug::{
Plugin,
ProcessContext
};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct SVFModel {
#[model(min = 10.0, max = 22000.0)]
#[parameter(name = "cutoff", label = "hz", gradient = "Exponential")]
cutoff: f32,
#[model(min = 0.0, max = 1.0)]
#[parameter(name = "resonance")]
resonance: f32
}
}
impl Default for SVFModel {
fn default() -> Self {
Self {
cutoff: 10000.0,
resonance: 0.6
}
}
}
struct SVFPlugin {
lpf: SVFSimper
}
impl Plugin for SVFPlugin {
const NAME: &'static str = "svf lowpass";
const PRODUCT: &'static str = "svf lowpass";
const VENDOR: &'static str = "spicy plugins & co";
const INPUT_CHANNELS: usize = 2;
const OUTPUT_CHANNELS: usize = 2;
type Model = SVFModel;
#[inline]
fn new(sample_rate: f32, model: &SVFModel) -> Self {
Self {
lpf: SVFSimper::new(model.cutoff, model.resonance, sample_rate)
}
}
#[inline]
fn process(&mut self, model: &SVFModelProcess, ctx: &mut ProcessContext<Self>) {
let input = &ctx.inputs[0].buffers;
let output = &mut ctx.outputs[0].buffers;
for i in 0..ctx.nframes {
self.lpf.set(model.cutoff[i], model.resonance[i], ctx.sample_rate);
let frame = f32x4::new(input[0][i], input[1][i], 0.0, 0.0);
let lp = self.lpf.process(frame);
// would be nice to align this, but doesn't seem possible with #[repr(align)].
// ah well. not much of a perf penalty for unaligned writes these days.
let mut frame_out = [0.0f32; 4];
unsafe {
lp.write_to_slice_unaligned_unchecked(&mut frame_out);
}
output[0][i] = frame_out[0];
output[1][i] = frame_out[1];
}
}
}
baseplug::vst2!(SVFPlugin, b"sVf!");

View File

@ -0,0 +1,56 @@
// implemented from https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
// thanks, andy!
use std::f32::consts;
use packed_simd::f32x4;
pub struct SVFSimper {
pub a1: f32x4,
pub a2: f32x4,
pub a3: f32x4,
pub ic1eq: f32x4,
pub ic2eq: f32x4
}
impl SVFSimper {
pub fn new(cutoff: f32, resonance: f32, sample_rate: f32) -> Self {
let g = (consts::PI * (cutoff / sample_rate)).tan();
let k = 2f32 - (1.9f32 * resonance.min(1f32).max(0f32));
let a1 = 1.0 / (1.0 + (g * (g + k)));
let a2 = g * a1;
let a3 = g * a2;
SVFSimper {
a1: f32x4::splat(a1),
a2: f32x4::splat(a2),
a3: f32x4::splat(a3),
ic1eq: f32x4::splat(0.0),
ic2eq: f32x4::splat(0.0)
}
}
pub fn set(&mut self, cutoff: f32, resonance: f32, sample_rate: f32) {
let new = Self::new(cutoff, resonance, sample_rate);
self.a1 = new.a1;
self.a2 = new.a2;
self.a3 = new.a3;
}
#[inline]
pub fn process(&mut self, v0: f32x4) -> f32x4 {
let v3 = v0 - self.ic2eq;
let v1 = (self.a1 * self.ic1eq) + (self.a2 * v3);
let v2 = self.ic2eq + (self.a2 * self.ic1eq) + (self.a3 * v3);
self.ic1eq = (2.0 * v1) - self.ic1eq;
self.ic2eq = (2.0 * v2) - self.ic2eq;
v2
}
}

View File

@ -0,0 +1,2 @@
#[macro_use]
pub mod vst2;

View File

@ -0,0 +1,144 @@
use std::os::raw::c_void;
use super::*;
macro_rules! adapter_from_effect {
($ptr:ident) => (
&mut *container_of!($ptr, VST2Adapter<T>, effect)
)
}
macro_rules! forward_to_adapter {
($method:ident, ($($arg:ident: $ty:ty),+), $ret:ty) => {
extern "C" fn $method<T: Plugin>(effect: *mut AEffect, $($arg: $ty,)+) -> $ret {
let adapter = unsafe { adapter_from_effect!(effect) };
adapter.$method($($arg,)+)
}
}
}
forward_to_adapter!(
dispatch,
(opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32),
isize);
forward_to_adapter!(
get_parameter,
(index: i32),
f32);
forward_to_adapter!(
set_parameter,
(index: i32, val: f32),
());
forward_to_adapter!(
process_replacing,
(in_buffers: *const *const f32, out_buffers: *mut *mut f32, nframes: i32),
());
extern "C" fn process_deprecated(_effect: *mut AEffect, _in: *const *const f32,
_out: *mut *mut f32, _nframes: i32)
{
}
extern "C" fn process_replacing_f64(_effect: *mut AEffect, _in: *const *const f64,
_out: *mut *mut f64, _nframes: i32)
{
}
pub fn plugin_main<P: Plugin>(host_cb: HostCallbackProc, unique_id: &[u8; 4]) -> *mut AEffect {
let mut flags = effect_flags::CAN_REPLACING | effect_flags::PROGRAM_CHUNKS;
if WrappedPlugin::<P>::wants_midi_input() {
flags |= effect_flags::IS_SYNTH;
}
if VST2Adapter::<P>::has_ui() {
flags |= effect_flags::HAS_EDITOR;
}
let unique_id =
(unique_id[0] as u32) << 24
| (unique_id[1] as u32) << 16
| (unique_id[2] as u32) << 8
| (unique_id[3] as u32);
let adapter = Box::new(VST2Adapter::<P> {
effect: AEffect {
magic: MAGIC,
dispatcher: dispatch::<P>,
process: process_deprecated,
set_parameter: set_parameter::<P>,
get_parameter: get_parameter::<P>,
num_programs: 0,
num_params: <P::Model as Model<P>>::Smooth::PARAMS.len() as i32,
num_inputs: P::INPUT_CHANNELS as i32,
num_outputs: P::OUTPUT_CHANNELS as i32,
flags: flags,
ptr_1: ptr::null_mut(),
ptr_2: ptr::null_mut(),
initial_delay: 0,
empty_2: [0; 8],
unknown_float: 0.0,
object: ptr::null_mut(),
user: ptr::null_mut(),
unique_id: unique_id as i32,
version: 0,
process_replacing: process_replacing::<P>,
process_double_replacing: process_replacing_f64,
},
host_cb,
editor_rect: Rect {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
wrapped: WrappedPlugin::new(),
state: None,
output_events_buffer: OutgoingEvents::new()
});
unsafe {
&mut ((*Box::into_raw(adapter)).effect)
}
}
#[macro_export]
macro_rules! vst2 {
($plugin:ty, $unique_id:expr) => {
#[cfg(crate_type="bin")]
std::compile_error!("vst2 requires an exported main() symbol, this will conflict for example with `cargo test` and non dynamic library crates.");
#[cfg(test)]
std::compile_error!("vst2 requires an exported main() symbol, this will conflict for example with `cargo test` and non dynamic library crates.");
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn main(host_callback: $crate::api::vst2::vst2_sys::HostCallbackProc) -> *mut $crate::api::vst2::vst2_sys::AEffect {
VSTPluginMain(host_callback)
}
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn VSTPluginMain(host_callback: $crate::api::vst2::vst2_sys::HostCallbackProc) -> *mut $crate::api::vst2::vst2_sys::AEffect {
$crate::api::vst2::plugin_main::<$plugin>(host_callback, $unique_id) as *mut $crate::api::vst2::vst2_sys::AEffect
}
}
}

View File

@ -0,0 +1,458 @@
use std::ffi::CStr;
use std::os::raw::c_void;
use std::ptr;
use std::{io, os::raw::c_char};
use std::{mem, slice};
pub use vst2_sys;
use vst2_sys::*;
use crate::wrapper::*;
use crate::*;
mod ui;
use ui::*;
mod abi;
pub use abi::plugin_main;
const MAX_PARAM_STR_LEN: usize = 32;
const MAX_EFFECT_NAME_LEN: usize = 32;
const MAX_VENDOR_STR_LEN: usize = 64;
const MAX_PRODUCT_STR_LEN: usize = 64;
const TRANSPORT_PLAYING: i32 = 2;
// output events buffer size
const OUTPUT_BUFFER_SIZE: usize = 256;
#[inline]
fn cstr_as_slice<'a>(ptr: *mut c_void, len: usize) -> &'a mut [u8] {
unsafe {
slice::from_raw_parts_mut(ptr as *mut u8, len)
}
}
fn cstrcpy(ptr: *mut c_void, src: &str, max_len: usize) {
let dest = cstr_as_slice(ptr, max_len);
let src_bytes = src.as_bytes();
let len = src_bytes.len().min(max_len - 1);
dest[..len].copy_from_slice(&src_bytes[..len]);
dest[len] = 0;
}
#[inline]
fn param_for_vst2_id<P, M>(id: i32) -> Option<&'static Param<P, M::Smooth>>
where
P: Plugin,
M: Model<P>,
{
M::Smooth::PARAMS.get(id as usize).copied()
}
macro_rules! param_for_idx {
($id:ident) => {
match param_for_vst2_id::<P, P::Model>($id) {
Some(p) => p,
None => return 0,
}
}
}
// represents an output buffer to send events to host
#[repr(C)]
pub struct OutgoingEvents {
num_events: i32,
_reserved: isize,
event_ptrs: [*mut MidiEvent; OUTPUT_BUFFER_SIZE],
events: [MidiEvent; OUTPUT_BUFFER_SIZE],
}
impl OutgoingEvents {
pub fn new() -> Self {
// create placeholders, ownership stays here
let blnk_evts = [vst2_sys::MidiEvent {
event_type: MIDI_TYPE,
byte_size: std::mem::size_of::<MidiEvent>() as i32,
delta_frames: 0,
flags: 0,
..unsafe { std::mem::zeroed() }
}; OUTPUT_BUFFER_SIZE];
// init ptrs to null
let evts_ptrs: [*mut MidiEvent; OUTPUT_BUFFER_SIZE] = [ptr::null_mut(); OUTPUT_BUFFER_SIZE];
OutgoingEvents {
num_events: 0,
_reserved: 0,
events: blnk_evts,
event_ptrs: evts_ptrs,
}
}
}
struct VST2Adapter<P: Plugin> {
effect: AEffect,
host_cb: HostCallbackProc,
wrapped: WrappedPlugin<P>,
editor_rect: Rect,
// when the VST2 host asks us for the chunk/data/state, the lifetime for that data extends
// until the *next* time that the host asks us for state. this means we have to just hold this
// around in memory indefinitely.
//
// allow(dead_code) here because we don't read from it after assignment, we only hold onto it
// here so that the host has access to it. compiler warns about "never read" without the allow.
#[allow(dead_code)]
state: Option<Vec<u8>>,
// output events buffer
output_events_buffer: OutgoingEvents,
}
impl<P: Plugin> VST2Adapter<P> {
#[inline]
fn dispatch(&mut self, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
match opcode {
////
// lifecycle
////
effect_opcodes::CLOSE => {
unsafe {
drop(Box::from_raw(self))
};
},
effect_opcodes::SET_SAMPLE_RATE => self.wrapped.set_sample_rate(opt),
effect_opcodes::MAINS_CHANGED => {
if value == 1 {
self.wrapped.reset();
}
},
////
// parameters
////
effect_opcodes::GET_PARAM_NAME => {
let param = param_for_idx!(index);
cstrcpy(ptr, param.get_name(), MAX_PARAM_STR_LEN);
return 0;
},
effect_opcodes::GET_PARAM_LABEL => {
let param = param_for_idx!(index);
cstrcpy(ptr, param.get_label(), MAX_PARAM_STR_LEN);
return 0;
},
effect_opcodes::GET_PARAM_DISPLAY => {
let param = param_for_idx!(index);
let dest = cstr_as_slice(ptr, MAX_PARAM_STR_LEN);
let mut cursor = io::Cursor::new(
&mut dest[..MAX_PARAM_STR_LEN - 1]);
match param.get_display(&self.wrapped.smoothed_model, &mut cursor) {
Ok(_) => {
let len = cursor.position();
dest[len as usize] = 0;
return len as isize;
},
Err(_) => {
dest[0] = 0;
return 0;
}
}
},
effect_opcodes::CAN_BE_AUTOMATED => return 1,
////
// plugin metadata
////
effect_opcodes::GET_EFFECT_NAME => {
cstrcpy(ptr, P::NAME, MAX_EFFECT_NAME_LEN);
return 1;
},
effect_opcodes::GET_PRODUCT_STRING => {
cstrcpy(ptr, P::PRODUCT, MAX_PRODUCT_STR_LEN);
return 1;
},
effect_opcodes::GET_VENDOR_STRING => {
cstrcpy(ptr, P::VENDOR, MAX_VENDOR_STR_LEN);
return 1;
},
////
// events
////
effect_opcodes::PROCESS_EVENTS => unsafe {
let vst_events = &*(ptr as *const Events);
let ev_slice = slice::from_raw_parts(
vst_events.events.as_ptr() as *const *const MidiEvent,
vst_events.num_events as usize
);
for ev in ev_slice {
if (**ev).event_type == MIDI_TYPE {
let ev = *ev as *const MidiEvent;
self.wrapped.midi_input(
(*ev).delta_frames as usize,
[(*ev).midi_data[0], (*ev).midi_data[1], (*ev).midi_data[2]]
);
}
}
return 0;
},
////
// state
////
effect_opcodes::GET_CHUNK => {
let new_state = match self.wrapped.serialise() {
None => return 0,
Some(s) => s
};
unsafe {
*(ptr as *mut *const c_void) =
new_state.as_ptr() as *const c_void;
}
let len = new_state.len() as isize;
self.state = Some(new_state);
return len;
},
effect_opcodes::SET_CHUNK => {
let state = unsafe {
slice::from_raw_parts(ptr as *mut u8, value as usize)
};
self.wrapped.deserialise(state);
return 0;
},
////
// editor
////
effect_opcodes::EDIT_GET_RECT => {
let ptr = ptr as *mut *mut c_void;
let (width, height) = match self.ui_get_rect() {
Some((w, h)) => (w, h),
None => unsafe {
*ptr = ptr::null_mut();
return 0;
}
};
self.editor_rect = Rect {
top: 0,
left: 0,
bottom: height,
right: width,
};
unsafe {
// we never read from editor_rect, just set it.
*ptr = (&self.editor_rect as *const _) as *mut c_void;
return 1;
}
},
effect_opcodes::EDIT_OPEN => {
return match self.ui_open(ptr) {
Ok(_) => 1,
Err(_) => 0,
};
},
effect_opcodes::EDIT_IDLE => {},
effect_opcodes::EDIT_CLOSE => {
self.ui_close();
},
effect_opcodes::CAN_DO => {
// get the property
let can_do = String::from_utf8_lossy(unsafe {
CStr::from_ptr(ptr as *mut c_char).to_bytes()
})
.into_owned();
let can_do = match can_do.as_str() {
"sendVstEvents" => 1,
"sendVstMidiEvent" => 1,
"receiveVstTimeInfo" => 1,
_otherwise => 0,
};
return can_do;
},
////
// ~who knows~
////
o => {
eprintln!("unhandled opcode {:?}", o);
},
}
0
}
#[inline]
fn get_parameter(&self, index: i32) -> f32 {
let param = match param_for_vst2_id::<P, P::Model>(index) {
Some(p) => p,
None => return 0.0
};
self.wrapped.get_parameter(param)
}
#[inline]
fn set_parameter(&mut self, index: i32, val: f32) {
let param = match param_for_vst2_id::<P, P::Model>(index) {
Some(p) => p,
None => return
};
self.wrapped.set_parameter(param, val);
}
fn get_musical_time(&mut self) -> MusicalTime {
let mut mtime = MusicalTime {
bpm: 0.0,
beat: 0.0,
is_playing: false
};
let time_info = {
let flags = time_info_flags::TEMPO_VALID | time_info_flags::PPQ_POS_VALID;
let vti = (self.host_cb)(&mut self.effect,
host_opcodes::GET_TIME, 0,
flags as isize,
ptr::null_mut(), 0.0);
match vti {
0 => return mtime,
ptr => unsafe { &*(ptr as *const TimeInfo) }
}
};
if (time_info.flags | time_info_flags::TEMPO_VALID) != 0 {
mtime.bpm = time_info.tempo;
}
if (time_info.flags | time_info_flags::PPQ_POS_VALID) != 0 {
mtime.beat = time_info.ppq_pos;
}
if (time_info.flags | TRANSPORT_PLAYING) != 0 {
mtime.is_playing = true;
}
mtime
}
#[inline]
fn process_replacing(
&mut self,
in_buffers: *const *const f32,
out_buffers: *mut *mut f32,
nframes: i32,
) {
let input = unsafe {
let b = slice::from_raw_parts(in_buffers, P::INPUT_CHANNELS);
let mut a: [&[f32]; 16] = Default::default();
for i in 0..P::INPUT_CHANNELS {
a[i] = slice::from_raw_parts(b[i], nframes as usize);
}
a
};
let output = unsafe {
let b = slice::from_raw_parts(out_buffers, P::OUTPUT_CHANNELS);
let mut a: [&mut [f32]; 16] = Default::default();
for i in 0..P::OUTPUT_CHANNELS {
a[i] = slice::from_raw_parts_mut(b[i], nframes as usize);
}
a
};
let musical_time = self.get_musical_time();
self.wrapped
.process(musical_time, input, output, nframes as usize);
// write output_events in the buffer
self.send_output_events();
// clear
self.wrapped.output_events.clear();
}
#[inline]
fn send_output_events(&mut self) {
self.output_events_buffer.num_events = 0;
// write into output buffer
for (bevt, ev) in self
.wrapped
.output_events
.iter()
.zip(self.output_events_buffer.events.iter_mut())
{
match bevt.data {
event::Data::Midi(midi_data) => {
let midi_event: MidiEvent = MidiEvent {
event_type: MIDI_TYPE,
byte_size: mem::size_of::<MidiEvent>() as i32,
delta_frames: bevt.frame as i32,
flags: 1,
note_length: 0,
note_offset: 0,
midi_data: [midi_data[0], midi_data[1], midi_data[2], 0],
detune: 0,
note_off_velocity: 0,
reserved_1: 0,
reserved_2: 0,
};
*ev = midi_event;
self.output_events_buffer.num_events += 1;
}
_ => {}
}
}
if self.output_events_buffer.num_events > 0 {
// update pointers
for (evt, evt_ptr) in self
.output_events_buffer
.events
.iter_mut()
.zip(self.output_events_buffer.event_ptrs.iter_mut())
{
*evt_ptr = evt as *mut MidiEvent;
}
// send to host
(self.host_cb)(&mut self.effect as *mut AEffect,
host_opcodes::PROCESS_EVENTS,
0, 0, &self.output_events_buffer as *const _ as *mut _, 0.0);
}
}
}

View File

@ -0,0 +1,103 @@
use std::os::raw::c_void;
use raw_window_handle::{RawWindowHandle, HasRawWindowHandle};
use super::*;
struct VST2WindowHandle(*mut c_void);
impl From<&VST2WindowHandle> for RawWindowHandle {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn from(handle: &VST2WindowHandle) -> RawWindowHandle {
use raw_window_handle::unix::*;
RawWindowHandle::Xcb(XcbHandle {
window: handle.0 as u32,
..XcbHandle::empty()
})
}
#[cfg(target_os = "windows")]
fn from(handle: &VST2WindowHandle) -> RawWindowHandle {
use raw_window_handle::windows::*;
RawWindowHandle::Windows(WindowsHandle {
hwnd: handle.0,
..WindowsHandle::empty()
})
}
#[cfg(target_os = "macos")]
fn from(handle: &VST2WindowHandle) -> RawWindowHandle {
use raw_window_handle::macos::*;
RawWindowHandle::MacOS(MacOSHandle {
ns_view: handle.0,
..MacOSHandle::empty()
})
}
}
unsafe impl HasRawWindowHandle for VST2WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
self.into()
}
}
pub(super) trait VST2UI {
fn has_ui() -> bool;
fn ui_get_rect(&self) -> Option<(i16, i16)>;
fn ui_open(&mut self, parent: *mut c_void) -> WindowOpenResult<()>;
fn ui_close(&mut self);
}
impl<P: Plugin> VST2UI for VST2Adapter<P> {
default fn has_ui() -> bool {
false
}
default fn ui_get_rect(&self) -> Option<(i16, i16)> {
None
}
default fn ui_open(&mut self, _parent: *mut c_void) -> WindowOpenResult<()> {
Err(())
}
default fn ui_close(&mut self) { }
}
impl<P: PluginUI> VST2UI for VST2Adapter<P> {
fn has_ui() -> bool {
true
}
fn ui_get_rect(&self) -> Option<(i16, i16)> {
Some(P::ui_size())
}
fn ui_open(&mut self, parent: *mut c_void) -> WindowOpenResult<()> {
let parent = VST2WindowHandle(parent);
if self.wrapped.ui_handle.is_none() {
P::ui_open(&parent)
.map(|handle| self.wrapped.ui_handle = Some(handle))
} else {
Ok(())
}
}
fn ui_close(&mut self) {
if let Some(handle) = self.wrapped.ui_handle.take() {
P::ui_close(handle)
}
}
}

View File

@ -0,0 +1,136 @@
use std::fmt;
use crate::{
Smooth,
SmoothStatus
};
const DECLICK_SETTLE: f32 = 0.001;
pub struct DeclickOutput<'a, T> {
pub from: &'a T,
pub to: &'a T,
pub fade: &'a [f32],
pub status: SmoothStatus
}
pub struct Declick<T: Sized + Clone> {
current: T,
next: Option<T>,
staged: Option<T>,
fade: Smooth<f32>
}
impl<T> Declick<T>
where T: Sized + Clone + Eq
{
pub fn new(initial: T) -> Self {
Self {
current: initial,
next: None,
staged: None,
fade: Smooth::new(0.0)
}
}
pub fn reset(&mut self, to: T) {
self.current = to;
self.next = None;
self.staged = None;
self.fade.reset(0.0);
}
pub fn set(&mut self, to: T) {
if self.dest() == &to {
return
}
if self.next.is_none() {
self.next = Some(to);
self.fade.reset(0.0);
self.fade.set(1.0);
} else {
self.staged = Some(to);
}
}
pub fn set_speed_ms(&mut self, sample_rate: f32, ms: f32) {
self.fade.set_speed_ms(sample_rate, ms);
}
#[inline]
pub fn output(&self) -> DeclickOutput<T> {
let fade = self.fade.output();
DeclickOutput {
from: &self.current,
to: self.next.as_ref().unwrap_or(&self.current),
fade: fade.values,
status: fade.status
}
}
#[inline]
pub fn current_value(&self) -> DeclickOutput<T> {
let fade = self.fade.current_value();
DeclickOutput {
from: &self.current,
to: self.next.as_ref().unwrap_or(&self.current),
fade: fade.values,
status: fade.status
}
}
#[inline]
pub fn dest(&self) -> &T {
self.staged.as_ref()
.or_else(|| self.next.as_ref())
.unwrap_or(&self.current)
}
#[inline]
pub fn is_active(&self) -> bool {
self.next.is_some()
}
#[inline]
pub fn process(&mut self, nframes: usize) {
self.fade.process(nframes);
}
pub fn update_status(&mut self) {
if !self.is_active() {
return;
}
self.fade.update_status_with_epsilon(DECLICK_SETTLE);
if self.fade.is_active() {
return;
}
self.current = self.next.take().unwrap();
self.next = self.staged.take();
}
}
impl<T> fmt::Debug for Declick<T>
where T: fmt::Debug + Sized + Clone
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(concat!("Declick<", stringify!(T), ">"))
.field("current", &self.current)
.field("next", &self.next)
.field("staged", &self.staged)
.field("fade", &self.fade)
.finish()
}
}

View File

@ -0,0 +1,51 @@
use std::fmt;
use crate::{
Plugin,
Model,
Param
};
pub enum Data<P: Plugin> {
Midi([u8; 3]),
Parameter {
param: &'static Param<P, <P::Model as Model<P>>::Smooth>,
val: f32
}
}
pub struct Event<P: Plugin> {
pub frame: usize,
pub data: Data<P>
}
////
// debug impls
////
impl<P: Plugin> fmt::Debug for Data<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Data::Midi(m) =>
f.debug_tuple("Data::Midi")
.field(&m)
.finish(),
Data::Parameter { param, val } =>
f.debug_struct("Data::Parameter")
.field("param", &param)
.field("val", &val)
.finish()
}
}
}
impl<P: Plugin> fmt::Debug for Event<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Event")
.field("frame", &self.frame)
.field("data", &self.data)
.finish()
}
}

View File

@ -0,0 +1,44 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
#![feature(specialization)]
#[macro_use]
pub mod util;
#[macro_use]
pub mod api;
mod smooth;
pub use smooth::{
Smooth,
SmoothOutput,
SmoothStatus
};
mod declick;
pub use declick::{
Declick,
DeclickOutput
};
pub mod event;
pub use event::Event;
mod model;
pub use model::*;
pub mod parameter;
pub use parameter::Param;
mod plugin;
pub use plugin::*;
mod time;
pub use time::*;
mod wrapper;
pub use baseplug_derive::model;
const MAX_BLOCKSIZE: usize = 128;

View File

@ -0,0 +1,26 @@
use crate::*;
pub trait Model<P: Plugin>: Sized + Default + 'static {
type Smooth:
SmoothModel<P, Self>
+ Parameters<P, Self::Smooth>;
}
pub trait SmoothModel<P: Plugin, T: Model<P>>: Sized + 'static{
type Process<'proc>;
fn from_model(model: T) -> Self;
fn as_model(&self) -> T;
fn set_sample_rate(&mut self, sample_rate: f32);
// set values from model with smoothing
fn set(&mut self, from: &T);
// set values from model without smoothing
fn reset(&mut self, from: &T);
fn current_value(&'_ mut self) -> Self::Process<'_>;
fn process(&'_ mut self, nframes: usize) -> Self::Process<'_>;
}

View File

@ -0,0 +1,192 @@
use std::fmt;
use std::io;
use crate::*;
use crate::util::*;
#[derive(Debug)]
pub enum Gradient {
Linear,
Power(f32),
Exponential
}
#[derive(Debug)]
pub enum Type {
Numeric {
min: f32,
max: f32,
gradient: Gradient
},
// eventually will have an Enum/Discrete type here
}
#[derive(Debug)]
pub enum Unit {
Generic,
Decibels
}
pub struct Format<P: Plugin, Model> {
pub display_cb: fn(&Param<P, Model>, &Model, &mut dyn io::Write) -> io::Result<()>,
pub label: &'static str
}
pub struct Param<P: Plugin, Model> {
pub name: &'static str,
pub short_name: Option<&'static str>,
pub unit: Unit,
pub param_type: Type,
pub format: Format<P, Model>,
pub dsp_notify: Option<fn(&mut P)>,
pub set_cb: fn(&Param<P, Model>, &mut Model, f32),
pub get_cb: fn(&Param<P, Model>, &Model) -> f32
}
impl<P: Plugin, Model> Param<P, Model> {
#[inline]
pub fn set(&self, model: &mut Model, val: f32) {
(self.set_cb)(self, model, val)
}
#[inline]
pub fn get(&self, model: &Model) -> f32 {
(self.get_cb)(self, model)
}
#[inline]
pub fn get_name(&self) -> &'static str {
self.short_name
.unwrap_or_else(|| self.name)
}
#[inline]
pub fn get_label(&self) -> &'static str {
if let Unit::Decibels = self.unit {
"dB"
} else {
self.format.label
}
}
#[inline]
pub fn get_display(&self, model: &Model, w: &mut dyn io::Write) -> io::Result<()> {
(self.format.display_cb)(self, model, w)
}
}
impl<P: Plugin, Model> fmt::Debug for Param<P, Model> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Param")
.field("name", &self.name)
.field("short_name", &self.short_name)
.field("unit", &self.unit)
.field("param_type", &self.param_type)
.finish()
}
}
pub trait Translatable<T, P: Plugin, Model> {
fn xlate_in(param: &Param<P, Model>, normalised: f32) -> T;
fn xlate_out(&self, param: &Param<P, Model>) -> f32;
}
impl<P: Plugin, Model> Translatable<f32, P, Model> for f32 {
fn xlate_in(param: &Param<P, Model>, normalised: f32) -> f32 {
let (min, max, gradient) = match &param.param_type {
Type::Numeric { min, max, gradient } => (min, max, gradient)
};
let normalised = normalised.min(1.0).max(0.0);
let map = |x: f32| -> f32 {
let range = max - min;
let mapped = (x * range) + min;
match param.unit {
Unit::Decibels => db_to_coeff(mapped),
_ => mapped
}
};
match gradient {
Gradient::Linear => map(normalised),
Gradient::Power(exponent) =>
map(normalised.powf(*exponent)),
Gradient::Exponential => {
if normalised == 0.0 {
return *min;
}
if normalised == 1.0 {
return *max;
}
let minl = min.log2();
let range = max.log2() - minl;
2.0f32.powf((normalised * range) + minl)
}
}
}
fn xlate_out(&self, param: &Param<P, Model>) -> f32 {
let (min, max, gradient) = match &param.param_type {
Type::Numeric { min, max, gradient } => (min, max, gradient)
};
if *self <= *min {
return 0.0;
}
if *self >= *max {
return 1.0;
}
let unmap = |x: f32| -> f32 {
let range = max - min;
let x = match param.unit {
Unit::Decibels => coeff_to_db(x),
_ => x
};
(x - min) / range
};
match gradient {
Gradient::Linear => unmap(*self),
Gradient::Power(exponent) =>
unmap(*self).powf(1.0 / *exponent),
Gradient::Exponential => {
let minl = min.log2();
let range = max.log2() - minl;
(self.log2() - minl) / range
}
}
}
}
pub trait TranslateFrom<F, T, P: Plugin, Model>
where T: Translatable<T, P, Model>
{
fn xlate_from(self, param: &Param<P, Model>) -> T;
}
impl<T, P: Plugin, Model> TranslateFrom<f32, T, P, Model> for f32
where T: Translatable<T, P, Model>
{
#[inline]
fn xlate_from(self, param: &Param<P, Model>) -> T {
T::xlate_in(param, self)
}
}

View File

@ -0,0 +1,81 @@
use serde::{
Serialize,
de::DeserializeOwned
};
use raw_window_handle::HasRawWindowHandle;
use crate::parameter::*;
use crate::event::*;
use crate::model::*;
use crate::time::*;
pub struct AudioBus<'a> {
pub connected_channels: isize,
pub buffers: &'a[&'a [f32]]
}
pub struct AudioBusMut<'a, 'b> {
pub connected_channels: isize,
pub buffers: &'a mut [&'b mut [f32]]
}
pub struct ProcessContext<'a, 'b, P: Plugin> {
pub nframes: usize,
pub sample_rate: f32,
pub inputs: &'a [AudioBus<'a>],
pub outputs: &'a mut [AudioBusMut<'a, 'b>],
pub enqueue_event: &'a mut dyn FnMut(Event<P>),
pub musical_time: &'a MusicalTime
}
pub trait Parameters<P: Plugin, Model: 'static> {
const PARAMS: &'static [&'static Param<P, Model>];
}
macro_rules! proc_model {
($plug:ident, $lifetime:lifetime) => {
<<$plug::Model as Model<$plug>>::Smooth as SmoothModel<$plug, $plug::Model>>::Process<$lifetime>
}
}
pub trait Plugin: Sized + Send + Sync + 'static {
const NAME: &'static str;
const PRODUCT: &'static str;
const VENDOR: &'static str;
const INPUT_CHANNELS: usize;
const OUTPUT_CHANNELS: usize;
type Model: Model<Self> + Serialize + DeserializeOwned;
fn new(sample_rate: f32, model: &Self::Model) -> Self;
fn process<'proc>(&mut self,
model: &proc_model!(Self, 'proc),
ctx: &'proc mut ProcessContext<Self>);
}
pub trait MidiReceiver: Plugin {
fn midi_input<'proc>(&mut self, model: &proc_model!(Self, 'proc),
data: [u8; 3]);
}
pub type WindowOpenResult<T> = Result<T, ()>;
pub trait PluginUI: Plugin {
type Handle;
fn ui_size() -> (i16, i16);
fn ui_open(parent: &impl HasRawWindowHandle) -> WindowOpenResult<Self::Handle>;
fn ui_close(handle: Self::Handle);
fn ui_param_notify(handle: &Self::Handle,
param: &'static Param<Self, <Self::Model as Model<Self>>::Smooth>, val: f32);
}

View File

@ -0,0 +1,194 @@
use std::fmt;
use std::ops;
use std::slice;
use num_traits::Float;
const SETTLE: f32 = 0.00001f32;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SmoothStatus {
Inactive,
Active,
Deactivating
}
impl SmoothStatus {
#[inline]
fn is_active(&self) -> bool {
self != &SmoothStatus::Inactive
}
}
pub struct SmoothOutput<'a, T> {
pub values: &'a [T],
pub status: SmoothStatus
}
impl<'a, T> SmoothOutput<'a, T> {
#[inline]
pub fn is_smoothing(&self) -> bool {
self.status.is_active()
}
}
impl<'a, T, I> ops::Index<I> for SmoothOutput<'a, T>
where I: slice::SliceIndex<[T]>
{
type Output = I::Output;
#[inline]
fn index(&self, idx: I) -> &I::Output {
&self.values[idx]
}
}
pub struct Smooth<T: Float> {
output: [T; crate::MAX_BLOCKSIZE],
input: T,
status: SmoothStatus,
a: T,
b: T,
last_output: T
}
impl<T> Smooth<T>
where T: Float + fmt::Display
{
pub fn new(input: T) -> Self {
Self {
status: SmoothStatus::Inactive,
input,
output: [input; crate::MAX_BLOCKSIZE],
a: T::one(),
b: T::zero(),
last_output: input
}
}
pub fn reset(&mut self, val: T)
{
*self = Self {
a: self.a,
b: self.b,
..Self::new(val)
};
}
pub fn set(&mut self, val: T) {
self.input = val;
self.status = SmoothStatus::Active;
}
#[inline]
pub fn dest(&self) -> T {
self.input
}
#[inline]
pub fn output(&self) -> SmoothOutput<T> {
SmoothOutput {
values: &self.output,
status: self.status
}
}
#[inline]
pub fn current_value(&self) -> SmoothOutput<T> {
SmoothOutput {
values: slice::from_ref(&self.last_output),
status: self.status
}
}
pub fn update_status_with_epsilon(&mut self, epsilon: T) -> SmoothStatus {
let status = self.status;
match status {
SmoothStatus::Active => {
if (self.input - self.output[0]).abs() < epsilon {
self.reset(self.input);
self.status = SmoothStatus::Deactivating;
}
},
SmoothStatus::Deactivating =>
self.status = SmoothStatus::Inactive,
_ => ()
};
self.status
}
pub fn process(&mut self, nframes: usize) {
if self.status != SmoothStatus::Active {
return
}
let nframes = nframes.min(crate::MAX_BLOCKSIZE);
let input = self.input * self.a;
self.output[0] = input + (self.last_output * self.b);
for i in 1..nframes {
self.output[i] = input + (self.output[i - 1] * self.b);
}
self.last_output = self.output[nframes - 1];
}
#[inline]
pub fn is_active(&self) -> bool {
self.status.is_active()
}
}
impl Smooth<f32> {
pub fn set_speed_ms(&mut self, sample_rate: f32, ms: f32) {
self.b = (-1.0f32 / (ms * (sample_rate / 1000.0f32))).exp();
self.a = 1.0f32 - self.b;
}
#[inline]
pub fn update_status(&mut self) -> SmoothStatus {
self.update_status_with_epsilon(SETTLE)
}
}
impl<T> From<T> for Smooth<T>
where T: Float + fmt::Display
{
fn from(val: T) -> Self {
Self::new(val)
}
}
impl<T, I> ops::Index<I> for Smooth<T>
where I: slice::SliceIndex<[T]>,
T: Float
{
type Output = I::Output;
#[inline]
fn index(&self, idx: I) -> &I::Output {
&self.output[idx]
}
}
impl<T> fmt::Debug for Smooth<T>
where T: Float + fmt::Debug
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(concat!("Smooth<", stringify!(T), ">"))
.field("output[0]", &self.output[0])
.field("input", &self.input)
.field("status", &self.status)
.field("last_output", &self.last_output)
.finish()
}
}

View File

@ -0,0 +1,15 @@
#[derive(Clone)]
pub struct MusicalTime {
pub bpm: f64,
pub beat: f64,
pub is_playing: bool
}
impl MusicalTime {
pub(crate) fn step_by_samples(&mut self, sample_rate: f64, samples: usize) {
let beats_per_second = self.bpm / 60f64;
let seconds = (samples as f64) / (sample_rate as f64);
self.beat += seconds * beats_per_second;
}
}

View File

@ -0,0 +1,29 @@
macro_rules! offset_of {
($container:ty, $field:ident) => (
&(*(::std::ptr::null_mut::<$container>())).$field as *const _ as usize
)
}
macro_rules! container_of {
($ptr:ident, $container:ty, $field:ident) => ({
(($ptr as usize) - offset_of!($container, $field)) as *mut $container
})
}
#[inline]
pub fn db_to_coeff(db: f32) -> f32 {
if db < -90.0 {
0.0
} else {
10.0f32.powf(0.05 * db)
}
}
#[inline]
pub fn coeff_to_db(coeff: f32) -> f32 {
if coeff <= 0.00003162277 {
-90.0
} else {
20.0 * coeff.log(10.0)
}
}

View File

@ -0,0 +1,358 @@
use crate::{
Model,
SmoothModel,
Plugin,
PluginUI,
MidiReceiver,
Param,
AudioBus,
AudioBusMut,
ProcessContext,
MusicalTime,
Event,
event
};
pub(crate) struct WrappedPlugin<P: Plugin> {
pub(crate) plug: P,
// even though it is *strongly forbidden* to allocate in the RT audio thread, many plugin APIs
// have no facilities for host-side allocation of event buffers which live through the
// subsequent `process()` call.
//
// the best we can do is pre-allocate a reasonably large buffer and hope we never have to
// enlarge it.
//
// see below in WrappedPlugin::new() for the capacity.
//
// XXX: there are *potential* threading issues with this. it would be completely possible for
// an enqueue_event() call to come *during* a process() call, and we need to be able to handle
// that in the future. we may need to use a different data structure here.
events: Vec<Event<P>>,
pub(crate) output_events: Vec<Event<P>>,
pub(crate) smoothed_model: <P::Model as Model<P>>::Smooth,
sample_rate: f32,
pub(crate) ui_handle: Option<<Self as WrappedPluginUI<P>>::UIHandle>
}
impl<P: Plugin> WrappedPlugin<P> {
#[inline]
pub(crate) fn new() -> Self {
Self {
plug: P::new(48000.0, &P::Model::default()),
events: Vec::with_capacity(512),
output_events: Vec::with_capacity(256),
smoothed_model:
<P::Model as Model<P>>::Smooth::from_model(P::Model::default()),
sample_rate: 0.0,
ui_handle: None
}
}
////
// lifecycle
////
#[inline]
pub(crate) fn set_sample_rate(&mut self, sample_rate: f32) {
self.sample_rate = sample_rate;
self.smoothed_model.set_sample_rate(sample_rate);
self.reset();
}
#[inline]
pub(crate) fn reset(&mut self) {
let model = self.smoothed_model.as_model();
self.plug = P::new(self.sample_rate, &model);
self.smoothed_model.reset(&model);
}
////
// parameters
////
#[inline]
pub(crate) fn get_parameter(&self, param: &Param<P, <P::Model as Model<P>>::Smooth>) -> f32 {
param.get(&self.smoothed_model)
}
#[inline]
pub(crate) fn set_parameter(&mut self, param: &'static Param<P, <P::Model as Model<P>>::Smooth>, val: f32) {
if param.dsp_notify.is_some() {
self.enqueue_event(Event {
frame: 0,
data: event::Data::Parameter {
param,
val
}
});
} else {
param.set(&mut self.smoothed_model, val);
}
self.ui_param_notify(param, val);
}
fn set_parameter_from_event(&mut self, param: &Param<P, <P::Model as Model<P>>::Smooth>, val: f32) {
param.set(&mut self.smoothed_model, val);
if let Some(dsp_notify) = param.dsp_notify {
dsp_notify(&mut self.plug);
}
}
////
// state
////
pub(crate) fn serialise(&self) -> Option<Vec<u8>>
{
let ser = self.smoothed_model.as_model();
serde_json::to_string(&ser)
.map(|s| s.into_bytes())
.ok()
}
pub(crate) fn deserialise<'de>(&mut self, data: &'de [u8]) {
let m: P::Model = match serde_json::from_slice(data) {
Ok(m) => m,
Err(_) => return
};
self.smoothed_model.set(&m);
}
////
// events
////
fn enqueue_event_in(ev: Event<P>, buffer: &mut Vec<Event<P>>) {
let latest_frame = match buffer.last() {
Some(ev) => ev.frame,
None => 0
};
if latest_frame <= ev.frame {
buffer.push(ev);
return;
}
let idx = buffer.iter()
.position(|e| e.frame > ev.frame)
.unwrap();
buffer.insert(idx, ev);
}
#[inline]
pub(crate) fn enqueue_event(&mut self, ev: Event<P>) {
Self::enqueue_event_in(ev, &mut self.events);
}
////
// process
////
#[inline]
fn dispatch_event(&mut self, ev_idx: usize) {
let ev = &self.events[ev_idx];
use event::Data;
match ev.data {
Data::Midi(m) => self.dispatch_midi_event(m),
Data::Parameter { param, val } => {
self.set_parameter_from_event(param, val);
}
}
}
#[inline]
pub(crate) fn process(
&mut self,
mut musical_time: MusicalTime,
input: [&[f32]; 16],
output: [&mut [f32]; 16],
mut nframes: usize,
) {
let mut start = 0;
let mut ev_idx = 0;
let [out_0, out_1, out_2, out_3, out_4, out_5, out_6, out_7, out_8, out_9, out_10, out_11, out_12, out_13, out_14, out_15] =
output;
while nframes > 0 {
let mut block_frames = nframes;
while ev_idx < self.events.len() && start == self.events[ev_idx].frame {
self.dispatch_event(ev_idx);
ev_idx += 1;
}
if ev_idx < self.events.len() {
block_frames = block_frames.min(self.events[ev_idx].frame - start);
}
block_frames = block_frames.min(crate::MAX_BLOCKSIZE);
let end = start + block_frames;
let mut a: [&[f32]; 16] = Default::default();
for i in 0..P::INPUT_CHANNELS {
a[i] = &input[i][start..end];
}
let in_bus = AudioBus {
connected_channels: P::INPUT_CHANNELS as isize,
buffers: &a[0..P::INPUT_CHANNELS],
};
macro_rules! helper {
($name:ident, $num:expr) => {
if $num < P::OUTPUT_CHANNELS {
&mut $name[start..end]
} else {
$name
}
};
}
let mut a: [&mut [f32]; 16] = [
helper!(out_0, 0),
helper!(out_1, 1),
helper!(out_2, 2),
helper!(out_3, 3),
helper!(out_4, 4),
helper!(out_5, 5),
helper!(out_6, 6),
helper!(out_7, 7),
helper!(out_8, 8),
helper!(out_9, 9),
helper!(out_10, 10),
helper!(out_11, 11),
helper!(out_12, 12),
helper!(out_13, 13),
helper!(out_14, 14),
helper!(out_15, 15),
];
let out_bus = AudioBusMut {
connected_channels: P::OUTPUT_CHANNELS as isize,
buffers: { &mut a[0..P::OUTPUT_CHANNELS] },
};
// this scope is here so that we drop ProcessContext right after we're done with it.
// since `enqueue_event()` holds a reference to `start`, we need to have that reference
// released when we update `start` at the bottom of the loop iteration.
{
let output_events = &mut self.output_events;
let mut context = ProcessContext {
nframes: block_frames,
sample_rate: self.sample_rate,
inputs: &[in_bus],
outputs: &mut [out_bus],
enqueue_event: &mut |mut ev| {
ev.frame += start;
Self::enqueue_event_in(ev, output_events);
},
musical_time: &musical_time
};
let proc_model = self.smoothed_model.process(block_frames);
self.plug.process(&proc_model, &mut context);
}
nframes -= block_frames;
start += block_frames;
musical_time.step_by_samples(self.sample_rate.into(), block_frames);
}
self.events.clear();
}
}
/////
// midi input
/////
pub(crate) trait WrappedPluginMidiInput {
fn wants_midi_input() -> bool;
fn midi_input(&mut self, frame: usize, data: [u8; 3]);
fn dispatch_midi_event(&mut self, data: [u8; 3]);
}
impl<T: Plugin> WrappedPluginMidiInput for WrappedPlugin<T> {
default fn wants_midi_input() -> bool {
false
}
default fn midi_input(&mut self, _frame: usize, _data: [u8; 3]) {
return
}
default fn dispatch_midi_event(&mut self, _data: [u8; 3]) {
return
}
}
impl<T: MidiReceiver> WrappedPluginMidiInput for WrappedPlugin<T> {
fn wants_midi_input() -> bool {
true
}
fn midi_input(&mut self, frame: usize, data: [u8; 3]) {
self.enqueue_event(Event {
frame,
data: event::Data::Midi(data)
})
}
fn dispatch_midi_event(&mut self, data: [u8; 3]) {
let model = self.smoothed_model.current_value();
self.plug.midi_input(&model, data)
}
}
/////
// UI
/////
pub(crate) trait WrappedPluginUI<P: Plugin> {
type UIHandle;
fn ui_param_notify(&self,
param: &'static Param<P, <P::Model as Model<P>>::Smooth>, val: f32);
}
impl<P: Plugin> WrappedPluginUI<P> for WrappedPlugin<P> {
default type UIHandle = ();
#[inline]
default fn ui_param_notify(&self,
_param: &'static Param<P, <P::Model as Model<P>>::Smooth>, _val: f32)
{
}
}
impl<P: PluginUI> WrappedPluginUI<P> for WrappedPlugin<P> {
type UIHandle = P::Handle;
#[inline]
fn ui_param_notify(&self,
param: &'static Param<P, <P::Model as Model<P>>::Smooth>, val: f32)
{
if let Some(ui_handle) = self.ui_handle.as_ref() {
P::ui_param_notify(ui_handle, param, val);
}
}
}

View File

@ -9,5 +9,4 @@ crate-type = ["cdylib"]
[dependencies]
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
serde = "1.0.126"
# utils = { path = "../utils" }
utils = { path = "../utils" }

View File

@ -7,21 +7,15 @@ use serde::{Deserialize, Serialize};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct GainModel {
#[model(min = -90.0, max = 3.0)]
#[parameter(name = "gain", unit = "Decibels",
gradient = "Power(0.15)")]
gain: f32
#[model(min = 0.0, max = 1.0)]
#[parameter(name = "gain")]
gain: f32,
}
}
impl Default for GainModel {
fn default() -> Self {
Self {
// "gain" is converted from dB to coefficient in the parameter handling code,
// so in the model here it's a coeff.
// -0dB == 1.0
gain: 1.0,
}
Self { gain: 1.0 }
}
}

15
crates/opiate/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "opiate"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
# baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
baseplug = { path = "../baseplug" }
serde = "1.0.126"
pvoc = { path = "../pvoc-rs" }
utils = { path = "../utils" }
log = "0.4.14"

114
crates/opiate/src/lib.rs Normal file
View File

@ -0,0 +1,114 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
// morphing algorithm from https://ccrma.stanford.edu/~jhsu/421b/
use baseplug::{Plugin, ProcessContext};
use pvoc::{Bin, PhaseVocoder};
use serde::{Deserialize, Serialize};
use utils::logs::*;
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct OpiateModel {
#[model(min = 0.0, max = 1.0)]
#[parameter(name = "morph")]
morph: f32,
}
}
impl Default for OpiateModel {
fn default() -> Self {
Self { morph: 0.0 }
}
}
struct Opiate {
pvoc: PhaseVocoder,
out_0: Vec<f32>,
out_1: Vec<f32>,
out_2: Vec<f32>,
out_3: Vec<f32>,
}
impl Plugin for Opiate {
const NAME: &'static str = "opiate";
const PRODUCT: &'static str = "opiate";
const VENDOR: &'static str = "unnieversal";
const INPUT_CHANNELS: usize = 4;
const OUTPUT_CHANNELS: usize = 2;
type Model = OpiateModel;
#[inline]
fn new(sample_rate: f32, _model: &OpiateModel) -> Self {
setup_logging("opiate.log");
Self {
pvoc: PhaseVocoder::new(4, sample_rate as f64, 64, 4),
out_0: vec![0.0; 300],
out_1: vec![0.0; 300],
out_2: vec![0.0; 300],
out_3: vec![0.0; 300],
}
}
#[inline]
fn process(&mut self, model: &OpiateModelProcess, ctx: &mut ProcessContext<Self>) {
let input = &ctx.inputs[0].buffers;
let output = &mut ctx.outputs[0].buffers;
if input.len() != 4 {
for i in 0..ctx.nframes {
output[0][i] = 0.0;
output[1][i] = 0.0;
}
return;
}
let out = &mut [
&mut self.out_0[..],
&mut self.out_1[..],
&mut self.out_2[..],
&mut self.out_3[..],
][..];
let morph = model.morph[0] as f64;
let imorph = 1.0 - morph;
self.pvoc.process(
input,
out,
|_channels: usize, bins: usize, input: &[Vec<Bin>], output: &mut [Vec<Bin>]| {
for j in 0..bins {
// TODO Check if working with the frequencies is the same as working with the phase
// i think we might need to try it to make sure it's the same
// to do that, we'll need to change how pvoc works
let mags = morph * (1.0 - input[0][j].amp) + input[0][j].amp;
let mags2 = imorph * (1.0 - input[2][j].amp) + input[2][j].amp;
let phases = input[0][j].freq - (input[0][j].freq * morph);
let phases2 = input[2][j].freq - (input[2][j].freq * imorph);
output[0][j].amp = mags * mags2;
output[0][j].freq = phases + phases2;
let mags = morph * (1.0 - input[1][j].amp) + input[1][j].amp;
let mags2 = imorph * (1.0 - input[3][j].amp) + input[3][j].amp;
let phases = input[1][j].freq - (input[1][j].freq * morph);
let phases2 = input[3][j].freq - (input[3][j].freq * imorph);
output[1][j].amp = mags * mags2;
output[1][j].freq = phases + phases2;
}
},
);
for i in 0..ctx.nframes {
output[0][i] = self.out_0[i];
output[1][i] = self.out_1[i];
}
}
}
baseplug::vst2!(Opiate, b"opiu");

View File

@ -0,0 +1,30 @@
/// Linear crossfade
/// x is the crossfading param, [0, 1]
/// Returns (fade_in, fade_out)
pub fn lin_crossfade(x: f32) -> (f32, f32) {
(x, 1.0 - x)
}
// next two are from https://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/
/// Amplitude preserving crossfade
/// x is the crossfading param, [0, 1]
/// Returns (fade_in, fade_out)
pub fn ap_crossfade(x: f32) -> (f32, f32) {
let fin = x * x * (3.0 - 2.0 * x);
(fin, 1.0 - fin)
}
/// Energy preserving crossfade
/// x is the crossfading param, [0, 1]
/// Returns (fade_in, fade_out)
pub fn ep_crossfade(x: f32) -> (f32, f32) {
let x2 = 1.0 - x;
let a = x * x2;
let b = a * (1.0 + 1.4186 * a);
let c = b + x;
let d = b + x2;
(c * c, d * d)
}

View File

@ -1,4 +1,5 @@
pub mod buffers;
pub mod crossfade;
pub mod delay;
pub mod envelope;
pub mod logs;

View File

@ -0,0 +1,14 @@
[package]
name = "velociter"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
rand = "0.8.4"
serde = "1.0.126"
# utils = { path = "../utils" }

View File

@ -0,0 +1,84 @@
#![allow(incomplete_features)]
#![feature(generic_associated_types)]
use baseplug::{event::Data, Event, MidiReceiver, Plugin, ProcessContext};
use serde::{Deserialize, Serialize};
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct VelociterModel {
#[model(min = 0.0, max = 127.9)]
#[parameter(name = "min_vel")]
min_vel: f32,
#[model(min = 0.0, max = 127.9)]
#[parameter(name = "max_vel")]
max_vel: f32,
}
}
impl Default for VelociterModel {
fn default() -> Self {
Self {
min_vel: 0.0,
max_vel: 127.9,
}
}
}
struct Velociter {
notes: Vec<[u8; 3]>,
}
impl Plugin for Velociter {
const NAME: &'static str = "velociter";
const PRODUCT: &'static str = "velociter";
const VENDOR: &'static str = "unnieversal";
const INPUT_CHANNELS: usize = 2;
const OUTPUT_CHANNELS: usize = 2;
type Model = VelociterModel;
#[inline]
fn new(_sample_rate: f32, _model: &VelociterModel) -> Self {
Self {
notes: Vec::with_capacity(300),
}
}
#[inline]
fn process(&mut self, model: &VelociterModelProcess, ctx: &mut ProcessContext<Self>) {
let input = &ctx.inputs[0].buffers;
let output = &mut ctx.outputs[0].buffers;
let enqueue_midi = &mut ctx.enqueue_event;
use rand::{thread_rng, Rng};
let mut rng = thread_rng();
for i in 0..ctx.nframes {
output[0][i] = input[0][i];
output[1][i] = input[1][i];
let min = model.min_vel[i];
let max = model.max_vel[i];
// make sure they're right
let (min, max) = (min.min(max), max.max(min));
for mut note in self.notes.drain(0..) {
note[2] = rng.gen_range(min..max).trunc() as u8;
let note = Event::<Velociter> {
frame: 0,
data: Data::Midi(note),
};
enqueue_midi(note);
}
}
}
}
impl MidiReceiver for Velociter {
fn midi_input(&mut self, _model: &VelociterModelProcess, data: [u8; 3]) {
self.notes.push(data);
}
}
baseplug::vst2!(Velociter, b"tAnE");