Compare commits
No commits in common. "f94653f3ec7dbe4e7fa9b016090dfa7efec9af49" and "e1b2004dcd8f416420d4cb712cbd9df11ea62486" have entirely different histories.
f94653f3ec
...
e1b2004dcd
11
README.md
11
README.md
|
@ -65,7 +65,6 @@ the following is the current list of plugins
|
|||
- `transmute_pitch`: pitch to midi converter
|
||||
- `reverter`: play sound backwards
|
||||
- `panera`: pan individual notes differently
|
||||
- `velociter`: random velocity setter
|
||||
|
||||
there's a bit of an explanation of each of the plugins below, but it's not a thorough documentation or a manual, it's just a bunch of notes i've written and a short description of the parameters
|
||||
|
||||
|
@ -288,16 +287,6 @@ there's three panning modes:
|
|||
|
||||
the peak detector is still a bit fiddly, so you'll have to tweak the params a bit until it works for the kind of sound you're giving it. the defaults have worked great for me, so i recommend you start from there and change as you see fit
|
||||
|
||||
### velociter
|
||||
|
||||
velociter sets random velocities to midi notes
|
||||
|
||||
params:
|
||||
- `min_vel`: minimum velocity
|
||||
- `max_vel`: maximum velocity
|
||||
|
||||
it'll set the velocity of every incoming midi note to be in the range `[min_vel, max_vel)`
|
||||
|
||||
## contributing
|
||||
|
||||
issues and prs are welcome, but please open an issue before making any big pr, i don't wanna have to reject a pr where you have put a lot of effort on. if you are fine with that, ig go ahead i'm not your mum
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
/target
|
||||
/baseplug-derive/target
|
||||
Cargo.lock
|
||||
.vscode
|
|
@ -1,76 +0,0 @@
|
|||
# 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
|
|
@ -1,38 +0,0 @@
|
|||
[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"]
|
|
@ -1,176 +0,0 @@
|
|||
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
|
|
@ -1,23 +0,0 @@
|
|||
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.
|
|
@ -1,36 +0,0 @@
|
|||
__ __
|
||||
| |--.---.-.-----.-----.-----.| |.--.--.-----.
|
||||
| _ | _ |__ --| -__| _ || || | | _ |
|
||||
|_____|___._|_____|_____| __||__||_____|___ |
|
||||
|__| |_____|
|
||||
|
||||
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
|
|
@ -1,15 +0,0 @@
|
|||
[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"
|
|
@ -1,15 +0,0 @@
|
|||
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()
|
||||
}
|
|
@ -1,559 +0,0 @@
|
|||
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 ),*
|
||||
];
|
||||
}
|
||||
};
|
||||
)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
# 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)
|
|
@ -1,62 +0,0 @@
|
|||
#![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");
|
|
@ -1,105 +0,0 @@
|
|||
#![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~");
|
|
@ -1,155 +0,0 @@
|
|||
#![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~");
|
|
@ -1,84 +0,0 @@
|
|||
#![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!");
|
|
@ -1,56 +0,0 @@
|
|||
// 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
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#[macro_use]
|
||||
pub mod vst2;
|
|
@ -1,144 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,458 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
#![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;
|
|
@ -1,26 +0,0 @@
|
|||
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<'_>;
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#[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;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,358 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,4 +9,5 @@ crate-type = ["cdylib"]
|
|||
[dependencies]
|
||||
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||
serde = "1.0.126"
|
||||
utils = { path = "../utils" }
|
||||
|
||||
# utils = { path = "../utils" }
|
||||
|
|
|
@ -7,15 +7,21 @@ use serde::{Deserialize, Serialize};
|
|||
baseplug::model! {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct GainModel {
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "gain")]
|
||||
gain: f32,
|
||||
#[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: 1.0 }
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "opiate"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||
baseplug = { path = "../baseplug" }
|
||||
serde = "1.0.126"
|
||||
pvoc = { path = "../pvoc-rs" }
|
||||
utils = { path = "../utils" }
|
||||
log = "0.4.14"
|
|
@ -1,114 +0,0 @@
|
|||
#![allow(incomplete_features)]
|
||||
#![feature(generic_associated_types)]
|
||||
|
||||
// morphing algorithm from https://ccrma.stanford.edu/~jhsu/421b/
|
||||
|
||||
use baseplug::{Plugin, ProcessContext};
|
||||
use pvoc::{Bin, PhaseVocoder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utils::logs::*;
|
||||
|
||||
baseplug::model! {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct OpiateModel {
|
||||
#[model(min = 0.0, max = 1.0)]
|
||||
#[parameter(name = "morph")]
|
||||
morph: f32,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OpiateModel {
|
||||
fn default() -> Self {
|
||||
Self { morph: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct Opiate {
|
||||
pvoc: PhaseVocoder,
|
||||
out_0: Vec<f32>,
|
||||
out_1: Vec<f32>,
|
||||
out_2: Vec<f32>,
|
||||
out_3: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Plugin for Opiate {
|
||||
const NAME: &'static str = "opiate";
|
||||
const PRODUCT: &'static str = "opiate";
|
||||
const VENDOR: &'static str = "unnieversal";
|
||||
|
||||
const INPUT_CHANNELS: usize = 4;
|
||||
const OUTPUT_CHANNELS: usize = 2;
|
||||
|
||||
type Model = OpiateModel;
|
||||
|
||||
#[inline]
|
||||
fn new(sample_rate: f32, _model: &OpiateModel) -> Self {
|
||||
setup_logging("opiate.log");
|
||||
|
||||
Self {
|
||||
pvoc: PhaseVocoder::new(4, sample_rate as f64, 64, 4),
|
||||
out_0: vec![0.0; 300],
|
||||
out_1: vec![0.0; 300],
|
||||
out_2: vec![0.0; 300],
|
||||
out_3: vec![0.0; 300],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn process(&mut self, model: &OpiateModelProcess, ctx: &mut ProcessContext<Self>) {
|
||||
let input = &ctx.inputs[0].buffers;
|
||||
let output = &mut ctx.outputs[0].buffers;
|
||||
|
||||
if input.len() != 4 {
|
||||
for i in 0..ctx.nframes {
|
||||
output[0][i] = 0.0;
|
||||
output[1][i] = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let out = &mut [
|
||||
&mut self.out_0[..],
|
||||
&mut self.out_1[..],
|
||||
&mut self.out_2[..],
|
||||
&mut self.out_3[..],
|
||||
][..];
|
||||
|
||||
let morph = model.morph[0] as f64;
|
||||
let imorph = 1.0 - morph;
|
||||
self.pvoc.process(
|
||||
input,
|
||||
out,
|
||||
|_channels: usize, bins: usize, input: &[Vec<Bin>], output: &mut [Vec<Bin>]| {
|
||||
for j in 0..bins {
|
||||
// TODO Check if working with the frequencies is the same as working with the phase
|
||||
// i think we might need to try it to make sure it's the same
|
||||
// to do that, we'll need to change how pvoc works
|
||||
|
||||
let mags = morph * (1.0 - input[0][j].amp) + input[0][j].amp;
|
||||
let mags2 = imorph * (1.0 - input[2][j].amp) + input[2][j].amp;
|
||||
let phases = input[0][j].freq - (input[0][j].freq * morph);
|
||||
let phases2 = input[2][j].freq - (input[2][j].freq * imorph);
|
||||
|
||||
output[0][j].amp = mags * mags2;
|
||||
output[0][j].freq = phases + phases2;
|
||||
|
||||
let mags = morph * (1.0 - input[1][j].amp) + input[1][j].amp;
|
||||
let mags2 = imorph * (1.0 - input[3][j].amp) + input[3][j].amp;
|
||||
let phases = input[1][j].freq - (input[1][j].freq * morph);
|
||||
let phases2 = input[3][j].freq - (input[3][j].freq * imorph);
|
||||
|
||||
output[1][j].amp = mags * mags2;
|
||||
output[1][j].freq = phases + phases2;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
for i in 0..ctx.nframes {
|
||||
output[0][i] = self.out_0[i];
|
||||
output[1][i] = self.out_1[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseplug::vst2!(Opiate, b"opiu");
|
|
@ -1,30 +0,0 @@
|
|||
/// Linear crossfade
|
||||
/// x is the crossfading param, [0, 1]
|
||||
/// Returns (fade_in, fade_out)
|
||||
pub fn lin_crossfade(x: f32) -> (f32, f32) {
|
||||
(x, 1.0 - x)
|
||||
}
|
||||
|
||||
// next two are from https://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/
|
||||
|
||||
/// Amplitude preserving crossfade
|
||||
/// x is the crossfading param, [0, 1]
|
||||
/// Returns (fade_in, fade_out)
|
||||
pub fn ap_crossfade(x: f32) -> (f32, f32) {
|
||||
let fin = x * x * (3.0 - 2.0 * x);
|
||||
|
||||
(fin, 1.0 - fin)
|
||||
}
|
||||
|
||||
/// Energy preserving crossfade
|
||||
/// x is the crossfading param, [0, 1]
|
||||
/// Returns (fade_in, fade_out)
|
||||
pub fn ep_crossfade(x: f32) -> (f32, f32) {
|
||||
let x2 = 1.0 - x;
|
||||
let a = x * x2;
|
||||
let b = a * (1.0 + 1.4186 * a);
|
||||
let c = b + x;
|
||||
let d = b + x2;
|
||||
|
||||
(c * c, d * d)
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
pub mod buffers;
|
||||
pub mod crossfade;
|
||||
pub mod delay;
|
||||
pub mod envelope;
|
||||
pub mod logs;
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
name = "velociter"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||
rand = "0.8.4"
|
||||
serde = "1.0.126"
|
||||
|
||||
# utils = { path = "../utils" }
|
|
@ -1,84 +0,0 @@
|
|||
#![allow(incomplete_features)]
|
||||
#![feature(generic_associated_types)]
|
||||
|
||||
use baseplug::{event::Data, Event, MidiReceiver, Plugin, ProcessContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
baseplug::model! {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct VelociterModel {
|
||||
#[model(min = 0.0, max = 127.9)]
|
||||
#[parameter(name = "min_vel")]
|
||||
min_vel: f32,
|
||||
#[model(min = 0.0, max = 127.9)]
|
||||
#[parameter(name = "max_vel")]
|
||||
max_vel: f32,
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VelociterModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_vel: 0.0,
|
||||
max_vel: 127.9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Velociter {
|
||||
notes: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
impl Plugin for Velociter {
|
||||
const NAME: &'static str = "velociter";
|
||||
const PRODUCT: &'static str = "velociter";
|
||||
const VENDOR: &'static str = "unnieversal";
|
||||
|
||||
const INPUT_CHANNELS: usize = 2;
|
||||
const OUTPUT_CHANNELS: usize = 2;
|
||||
|
||||
type Model = VelociterModel;
|
||||
|
||||
#[inline]
|
||||
fn new(_sample_rate: f32, _model: &VelociterModel) -> Self {
|
||||
Self {
|
||||
notes: Vec::with_capacity(300),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn process(&mut self, model: &VelociterModelProcess, ctx: &mut ProcessContext<Self>) {
|
||||
let input = &ctx.inputs[0].buffers;
|
||||
let output = &mut ctx.outputs[0].buffers;
|
||||
let enqueue_midi = &mut ctx.enqueue_event;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
let mut rng = thread_rng();
|
||||
|
||||
for i in 0..ctx.nframes {
|
||||
output[0][i] = input[0][i];
|
||||
output[1][i] = input[1][i];
|
||||
|
||||
let min = model.min_vel[i];
|
||||
let max = model.max_vel[i];
|
||||
// make sure they're right
|
||||
let (min, max) = (min.min(max), max.max(min));
|
||||
|
||||
for mut note in self.notes.drain(0..) {
|
||||
note[2] = rng.gen_range(min..max).trunc() as u8;
|
||||
let note = Event::<Velociter> {
|
||||
frame: 0,
|
||||
data: Data::Midi(note),
|
||||
};
|
||||
enqueue_midi(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MidiReceiver for Velociter {
|
||||
fn midi_input(&mut self, _model: &VelociterModelProcess, data: [u8; 3]) {
|
||||
self.notes.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
baseplug::vst2!(Velociter, b"tAnE");
|
Loading…
Reference in New Issue