diff --git a/crates/baseplug/.gitignore b/crates/baseplug/.gitignore new file mode 100644 index 0000000..b5a0b70 --- /dev/null +++ b/crates/baseplug/.gitignore @@ -0,0 +1,4 @@ +/target +/baseplug-derive/target +Cargo.lock +.vscode \ No newline at end of file diff --git a/crates/baseplug/CODE_OF_CONDUCT.md b/crates/baseplug/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fd28ea2 --- /dev/null +++ b/crates/baseplug/CODE_OF_CONDUCT.md @@ -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 diff --git a/crates/baseplug/Cargo.toml b/crates/baseplug/Cargo.toml new file mode 100644 index 0000000..64b56f3 --- /dev/null +++ b/crates/baseplug/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "baseplug" +version = "0.1.0" +authors = ["William Light "] +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"] diff --git a/crates/baseplug/LICENSE-APACHE b/crates/baseplug/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/crates/baseplug/LICENSE-APACHE @@ -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 diff --git a/crates/baseplug/LICENSE-MIT b/crates/baseplug/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/crates/baseplug/LICENSE-MIT @@ -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. diff --git a/crates/baseplug/README b/crates/baseplug/README new file mode 100644 index 0000000..693b4e5 --- /dev/null +++ b/crates/baseplug/README @@ -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 diff --git a/crates/baseplug/baseplug-derive/Cargo.toml b/crates/baseplug/baseplug-derive/Cargo.toml new file mode 100644 index 0000000..7a6e591 --- /dev/null +++ b/crates/baseplug/baseplug-derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "baseplug-derive" +version = "0.1.0" +authors = ["William Light "] +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" diff --git a/crates/baseplug/baseplug-derive/src/lib.rs b/crates/baseplug/baseplug-derive/src/lib.rs new file mode 100644 index 0000000..ce46c08 --- /dev/null +++ b/crates/baseplug/baseplug-derive/src/lib.rs @@ -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() +} diff --git a/crates/baseplug/baseplug-derive/src/model.rs b/crates/baseplug/baseplug-derive/src/model.rs new file mode 100644 index 0000000..e4abb73 --- /dev/null +++ b/crates/baseplug/baseplug-derive/src/model.rs @@ -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, + label: Option, + unit: Option, + gradient: Option, + dsp_notify: Option +} + +struct FieldInfo<'a> { + vis: &'a Visibility, + ident: &'a Ident, + ty: &'a Type, + + wrapping: Option, + + bounds: ModelBounds, + smooth_ms: f32, + + parameter_info: Option +} + +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) { + 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) { + 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 { + let param = match self.parameter_info { + Some(ref p) => p, + None => return None + }; + + let pty = quote!(::baseplug::Param); + + let ident = &self.ident; + let name = ¶m.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 ::baseplug::Model

for #model_name { + type Smooth = #smoothed_ident; + } + + #[doc(hidden)] + impl ::baseplug::SmoothModel 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 ::baseplug::Parameters for #smoothed_ident { + const PARAMS: &'static [&'static ::baseplug::Param] = &[ + #( & #parameters ),* + ]; + } + }; + ) +} diff --git a/crates/baseplug/doc/plugin_api_notes.md b/crates/baseplug/doc/plugin_api_notes.md new file mode 100644 index 0000000..449a9f6 --- /dev/null +++ b/crates/baseplug/doc/plugin_api_notes.md @@ -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) diff --git a/crates/baseplug/examples/gain.rs b/crates/baseplug/examples/gain.rs new file mode 100644 index 0000000..51bf7b5 --- /dev/null +++ b/crates/baseplug/examples/gain.rs @@ -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) { + 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"); diff --git a/crates/baseplug/examples/midi_out_metronome.rs b/crates/baseplug/examples/midi_out_metronome.rs new file mode 100644 index 0000000..84de169 --- /dev/null +++ b/crates/baseplug/examples/midi_out_metronome.rs @@ -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) + { + 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:: { + 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:: { + 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~"); diff --git a/crates/baseplug/examples/midi_sine.rs b/crates/baseplug/examples/midi_sine.rs new file mode 100644 index 0000000..eddc27e --- /dev/null +++ b/crates/baseplug/examples/midi_sine.rs @@ -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) { + 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~"); diff --git a/crates/baseplug/examples/svf/main.rs b/crates/baseplug/examples/svf/main.rs new file mode 100644 index 0000000..92a53f5 --- /dev/null +++ b/crates/baseplug/examples/svf/main.rs @@ -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) { + 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!"); diff --git a/crates/baseplug/examples/svf/svf_simper.rs b/crates/baseplug/examples/svf/svf_simper.rs new file mode 100644 index 0000000..231a9ec --- /dev/null +++ b/crates/baseplug/examples/svf/svf_simper.rs @@ -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 + } +} diff --git a/crates/baseplug/src/api/mod.rs b/crates/baseplug/src/api/mod.rs new file mode 100644 index 0000000..be3b9f3 --- /dev/null +++ b/crates/baseplug/src/api/mod.rs @@ -0,0 +1,2 @@ +#[macro_use] +pub mod vst2; diff --git a/crates/baseplug/src/api/vst2/abi.rs b/crates/baseplug/src/api/vst2/abi.rs new file mode 100644 index 0000000..106b4e1 --- /dev/null +++ b/crates/baseplug/src/api/vst2/abi.rs @@ -0,0 +1,144 @@ +use std::os::raw::c_void; + + +use super::*; + + +macro_rules! adapter_from_effect { + ($ptr:ident) => ( + &mut *container_of!($ptr, VST2Adapter, effect) + ) +} + +macro_rules! forward_to_adapter { + ($method:ident, ($($arg:ident: $ty:ty),+), $ret:ty) => { + extern "C" fn $method(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(host_cb: HostCallbackProc, unique_id: &[u8; 4]) -> *mut AEffect { + let mut flags = effect_flags::CAN_REPLACING | effect_flags::PROGRAM_CHUNKS; + + if WrappedPlugin::

::wants_midi_input() { + flags |= effect_flags::IS_SYNTH; + } + + if VST2Adapter::

::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::

{ + effect: AEffect { + magic: MAGIC, + + dispatcher: dispatch::

, + process: process_deprecated, + set_parameter: set_parameter::

, + get_parameter: get_parameter::

, + + num_programs: 0, + num_params: >::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::

, + 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 + } + } +} diff --git a/crates/baseplug/src/api/vst2/mod.rs b/crates/baseplug/src/api/vst2/mod.rs new file mode 100644 index 0000000..31e617e --- /dev/null +++ b/crates/baseplug/src/api/vst2/mod.rs @@ -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(id: i32) -> Option<&'static Param> + where + P: Plugin, + M: Model

, +{ + M::Smooth::PARAMS.get(id as usize).copied() +} + +macro_rules! param_for_idx { + ($id:ident) => { + match param_for_vst2_id::($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::() 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 { + effect: AEffect, + host_cb: HostCallbackProc, + wrapped: WrappedPlugin

, + + 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>, + + // output events buffer + output_events_buffer: OutgoingEvents, +} + +impl VST2Adapter

{ + #[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::(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::(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::() 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); + } + } +} diff --git a/crates/baseplug/src/api/vst2/ui.rs b/crates/baseplug/src/api/vst2/ui.rs new file mode 100644 index 0000000..f20dc67 --- /dev/null +++ b/crates/baseplug/src/api/vst2/ui.rs @@ -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 VST2UI for VST2Adapter

{ + 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 VST2UI for VST2Adapter

{ + 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) + } + } +} diff --git a/crates/baseplug/src/declick.rs b/crates/baseplug/src/declick.rs new file mode 100644 index 0000000..1ff1772 --- /dev/null +++ b/crates/baseplug/src/declick.rs @@ -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 { + current: T, + next: Option, + staged: Option, + + fade: Smooth +} + +impl Declick + 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 { + 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 { + 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 fmt::Debug for Declick + 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() + } +} diff --git a/crates/baseplug/src/event.rs b/crates/baseplug/src/event.rs new file mode 100644 index 0000000..209a495 --- /dev/null +++ b/crates/baseplug/src/event.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use crate::{ + Plugin, + Model, + Param +}; + +pub enum Data { + Midi([u8; 3]), + + Parameter { + param: &'static Param>::Smooth>, + val: f32 + } +} + +pub struct Event { + pub frame: usize, + pub data: Data

+} + +//// +// debug impls +//// + +impl fmt::Debug for Data

{ + 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", ¶m) + .field("val", &val) + .finish() + } + } +} + +impl fmt::Debug for Event

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Event") + .field("frame", &self.frame) + .field("data", &self.data) + .finish() + } +} diff --git a/crates/baseplug/src/lib.rs b/crates/baseplug/src/lib.rs new file mode 100644 index 0000000..c4ef868 --- /dev/null +++ b/crates/baseplug/src/lib.rs @@ -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; diff --git a/crates/baseplug/src/model.rs b/crates/baseplug/src/model.rs new file mode 100644 index 0000000..02a0d82 --- /dev/null +++ b/crates/baseplug/src/model.rs @@ -0,0 +1,26 @@ +use crate::*; + + +pub trait Model: Sized + Default + 'static { + type Smooth: + SmoothModel + + Parameters; +} + +pub trait SmoothModel>: 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<'_>; +} diff --git a/crates/baseplug/src/parameter.rs b/crates/baseplug/src/parameter.rs new file mode 100644 index 0000000..e463c2f --- /dev/null +++ b/crates/baseplug/src/parameter.rs @@ -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 { + pub display_cb: fn(&Param, &Model, &mut dyn io::Write) -> io::Result<()>, + pub label: &'static str +} + +pub struct Param { + pub name: &'static str, + pub short_name: Option<&'static str>, + + pub unit: Unit, + + pub param_type: Type, + pub format: Format, + + pub dsp_notify: Option, + + pub set_cb: fn(&Param, &mut Model, f32), + pub get_cb: fn(&Param, &Model) -> f32 +} + +impl Param { + #[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 fmt::Debug for Param { + 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 { + fn xlate_in(param: &Param, normalised: f32) -> T; + fn xlate_out(&self, param: &Param) -> f32; +} + +impl Translatable for f32 { + fn xlate_in(param: &Param, normalised: f32) -> f32 { + let (min, max, gradient) = match ¶m.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) -> f32 { + let (min, max, gradient) = match ¶m.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 + where T: Translatable +{ + fn xlate_from(self, param: &Param) -> T; +} + +impl TranslateFrom for f32 + where T: Translatable +{ + #[inline] + fn xlate_from(self, param: &Param) -> T { + T::xlate_in(param, self) + } +} diff --git a/crates/baseplug/src/plugin.rs b/crates/baseplug/src/plugin.rs new file mode 100644 index 0000000..909273e --- /dev/null +++ b/crates/baseplug/src/plugin.rs @@ -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

), + + pub musical_time: &'a MusicalTime +} + +pub trait Parameters { + const PARAMS: &'static [&'static Param]; +} + +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 + 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); +} + +pub trait MidiReceiver: Plugin { + fn midi_input<'proc>(&mut self, model: &proc_model!(Self, 'proc), + data: [u8; 3]); +} + +pub type WindowOpenResult = Result; + +pub trait PluginUI: Plugin { + type Handle; + + fn ui_size() -> (i16, i16); + + fn ui_open(parent: &impl HasRawWindowHandle) -> WindowOpenResult; + fn ui_close(handle: Self::Handle); + + fn ui_param_notify(handle: &Self::Handle, + param: &'static Param>::Smooth>, val: f32); +} diff --git a/crates/baseplug/src/smooth.rs b/crates/baseplug/src/smooth.rs new file mode 100644 index 0000000..95f3317 --- /dev/null +++ b/crates/baseplug/src/smooth.rs @@ -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 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 { + output: [T; crate::MAX_BLOCKSIZE], + input: T, + + status: SmoothStatus, + + a: T, + b: T, + last_output: T +} + +impl Smooth + 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 { + SmoothOutput { + values: &self.output, + status: self.status + } + } + + #[inline] + pub fn current_value(&self) -> SmoothOutput { + 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 { + 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 From for Smooth + where T: Float + fmt::Display +{ + fn from(val: T) -> Self { + Self::new(val) + } +} + +impl ops::Index for Smooth + where I: slice::SliceIndex<[T]>, + T: Float +{ + type Output = I::Output; + + #[inline] + fn index(&self, idx: I) -> &I::Output { + &self.output[idx] + } +} + +impl fmt::Debug for Smooth + 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() + } +} diff --git a/crates/baseplug/src/time.rs b/crates/baseplug/src/time.rs new file mode 100644 index 0000000..936fbfa --- /dev/null +++ b/crates/baseplug/src/time.rs @@ -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; + } +} diff --git a/crates/baseplug/src/util.rs b/crates/baseplug/src/util.rs new file mode 100644 index 0000000..ed5c2ae --- /dev/null +++ b/crates/baseplug/src/util.rs @@ -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) + } +} diff --git a/crates/baseplug/src/wrapper.rs b/crates/baseplug/src/wrapper.rs new file mode 100644 index 0000000..730a4ee --- /dev/null +++ b/crates/baseplug/src/wrapper.rs @@ -0,0 +1,358 @@ +use crate::{ + Model, + SmoothModel, + + Plugin, + PluginUI, + MidiReceiver, + Param, + + AudioBus, + AudioBusMut, + ProcessContext, + MusicalTime, + + Event, + event +}; + +pub(crate) struct WrappedPlugin { + 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>, + pub(crate) output_events: Vec>, + + pub(crate) smoothed_model: >::Smooth, + sample_rate: f32, + + pub(crate) ui_handle: Option<>::UIHandle> +} + +impl WrappedPlugin

{ + #[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: + >::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>::Smooth>) -> f32 { + param.get(&self.smoothed_model) + } + + #[inline] + pub(crate) fn set_parameter(&mut self, param: &'static Param>::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>::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> + { + 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

, buffer: &mut Vec>) { + 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

) { + 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 WrappedPluginMidiInput for WrappedPlugin { + 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 WrappedPluginMidiInput for WrappedPlugin { + 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 { + type UIHandle; + + fn ui_param_notify(&self, + param: &'static Param>::Smooth>, val: f32); +} + +impl WrappedPluginUI

for WrappedPlugin

{ + default type UIHandle = (); + + #[inline] + default fn ui_param_notify(&self, + _param: &'static Param>::Smooth>, _val: f32) + { + } +} + +impl WrappedPluginUI

for WrappedPlugin

{ + type UIHandle = P::Handle; + + #[inline] + fn ui_param_notify(&self, + param: &'static Param>::Smooth>, val: f32) + { + if let Some(ui_handle) = self.ui_handle.as_ref() { + P::ui_param_notify(ui_handle, param, val); + } + } +}