add the baseplug code directly cause idk how to use submodules :)
parent
37e9626b24
commit
37f36421ef
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
/baseplug-derive/target
|
||||||
|
Cargo.lock
|
||||||
|
.vscode
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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"
|
|
@ -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()
|
||||||
|
}
|
|
@ -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 = ¶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<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 ),*
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
|
@ -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");
|
|
@ -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~");
|
|
@ -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~");
|
|
@ -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!");
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
#[macro_use]
|
||||||
|
pub mod vst2;
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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", ¶m)
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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<'_>;
|
||||||
|
}
|
|
@ -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 ¶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<P, Model>) -> 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<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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue