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
|
- `transmute_pitch`: pitch to midi converter
|
||||||
- `reverter`: play sound backwards
|
- `reverter`: play sound backwards
|
||||||
- `panera`: pan individual notes differently
|
- `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
|
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
|
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
|
## contributing
|
||||||
|
|
||||||
issues and prs are welcome, but please open an issue before making any big pr, i don't wanna have to reject a pr where you have put a lot of effort on. if you are fine with that, ig go ahead i'm not your mum
|
issues and prs are welcome, but please open an issue before making any big pr, i don't wanna have to reject a pr where you have put a lot of effort on. if you are fine with that, ig go ahead i'm not your mum
|
||||||
|
|
|
@ -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]
|
[dependencies]
|
||||||
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" }
|
||||||
serde = "1.0.126"
|
serde = "1.0.126"
|
||||||
utils = { path = "../utils" }
|
|
||||||
|
# utils = { path = "../utils" }
|
||||||
|
|
|
@ -7,15 +7,21 @@ use serde::{Deserialize, Serialize};
|
||||||
baseplug::model! {
|
baseplug::model! {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct GainModel {
|
struct GainModel {
|
||||||
#[model(min = 0.0, max = 1.0)]
|
#[model(min = -90.0, max = 3.0)]
|
||||||
#[parameter(name = "gain")]
|
#[parameter(name = "gain", unit = "Decibels",
|
||||||
gain: f32,
|
gradient = "Power(0.15)")]
|
||||||
|
gain: f32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GainModel {
|
impl Default for GainModel {
|
||||||
fn default() -> Self {
|
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 buffers;
|
||||||
pub mod crossfade;
|
|
||||||
pub mod delay;
|
pub mod delay;
|
||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
pub mod logs;
|
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