mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-05-01 06:30:13 +02:00
relayer arranger view and extract button_2 and button_3 to top level
This commit is contained in:
parent
d1bb33dc41
commit
fe9d5a309e
midi/src
plugin/vst
.github/workflows
.gitignoreCHANGELOG.mdCargo.tomlLICENSEREADME.mdexamples
dimension_expander.rsfwd_midi.rsgain_effect.rsladder_filter.rssimple_host.rssine_synth.rstransfer_and_smooth.rs
osx_vst_bundler.shrustfmt.tomlsrc
tek/src
|
@ -53,7 +53,9 @@ content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
|
|||
)));
|
||||
|
||||
impl PianoHorizontal {
|
||||
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
/// Draw the piano roll background.
|
||||
///
|
||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
|
||||
for (y, note) in (0..=127).rev().enumerate() {
|
||||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||||
|
@ -81,7 +83,9 @@ impl PianoHorizontal {
|
|||
}
|
||||
}
|
||||
}
|
||||
/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
/// Draw the piano roll foreground.
|
||||
///
|
||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
||||
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
|
||||
let mut notes_on = [false;128];
|
||||
|
@ -251,8 +255,8 @@ impl MidiViewer for PianoHorizontal {
|
|||
(clip.length / self.range.time_zoom().get(), 128)
|
||||
}
|
||||
fn redraw (&self) {
|
||||
let buffer = if let Some(clip) = self.clip.as_ref() {
|
||||
let clip = clip.read().unwrap();
|
||||
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
|
||||
let clip = clip.read().unwrap();
|
||||
let buf_size = self.buffer_size(&clip);
|
||||
let mut buffer = BigBuffer::from(buf_size);
|
||||
let note_len = self.note_len();
|
||||
|
@ -263,8 +267,7 @@ impl MidiViewer for PianoHorizontal {
|
|||
buffer
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
*self.buffer.write().unwrap() = buffer
|
||||
}
|
||||
}
|
||||
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.clip_mut() = clip.cloned();
|
||||
|
|
33
plugin/vst/.github/workflows/deploy.yml
vendored
Normal file
33
plugin/vst/.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||
- name: Set up CI environment
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
# Sanity check: make sure the release builds
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
# Sanity check: make sure all tests in the release pass
|
||||
- name: Test
|
||||
run: cargo test --verbose
|
||||
|
||||
# Deploy to crates.io
|
||||
# Only works on github releases (tagged commits)
|
||||
- name: Deploy to crates.io
|
||||
env:
|
||||
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||
run: cargo publish --token $CRATES_IO_TOKEN --manifest-path Cargo.toml
|
46
plugin/vst/.github/workflows/docs.yml
vendored
Normal file
46
plugin/vst/.github/workflows/docs.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||
- name: Set up CI environment
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
# Sanity check: make sure the release builds
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
# Sanity check: make sure all tests in the release pass
|
||||
- name: Test
|
||||
run: cargo test --verbose
|
||||
|
||||
# Generate docs
|
||||
# TODO: what does the last line here do?
|
||||
- name: Generate docs
|
||||
env:
|
||||
GH_ENCRYPTED_TOKEN: ${{ secrets.GH_ENCRYPTED_TOKEN }}
|
||||
run: |
|
||||
cargo doc --all --no-deps
|
||||
echo '<meta http-equiv=refresh content=0;url=vst/index.html>' > target/doc/index.html
|
||||
|
||||
# Push docs to github pages (branch `gh-pages`)
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: target/doc
|
38
plugin/vst/.github/workflows/rust.yml
vendored
Normal file
38
plugin/vst/.github/workflows/rust.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
name: Rust
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||
- name: Set up CI environment
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
# Makes sure the code builds successfully.
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
|
||||
# Makes sure all of the tests pass.
|
||||
- name: Test
|
||||
run: cargo test --verbose
|
||||
|
||||
# Runs Clippy on the codebase, and makes sure there are no lint warnings.
|
||||
# Disabled for now. Re-enable if you find it useful enough to deal with it constantly breaking.
|
||||
# - name: Clippy
|
||||
# run: cargo clippy --all-targets --all-features -- -D warnings -A clippy::unreadable_literal -A clippy::needless_range_loop -A clippy::float_cmp -A clippy::comparison-chain -A clippy::needless-doctest-main -A clippy::missing-safety-doc
|
||||
|
||||
# Makes sure the codebase is up to `cargo fmt` standards
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
21
plugin/vst/.gitignore
vendored
Normal file
21
plugin/vst/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Compiled files
|
||||
*.o
|
||||
*.so
|
||||
*.rlib
|
||||
*.dll
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
|
||||
# Generated by Cargo
|
||||
/target/
|
||||
/examples/*/target/
|
||||
Cargo.lock
|
||||
|
||||
# Vim
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
86
plugin/vst/CHANGELOG.md
Normal file
86
plugin/vst/CHANGELOG.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Added deprecation notice.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- `SysExEvent` no longer contains invalid data on 64-bit systems ([#170](https://github.com/RustAudio/vst-rs/pull/171)]
|
||||
- Function pointers in `AEffect` marked as `extern` ([#141](https://github.com/RustAudio/vst-rs/pull/141))
|
||||
- Key character fixes ([#152](https://github.com/RustAudio/vst-rs/pull/152))
|
||||
- Doc and deploy actions fixes ([9eb1bef](https://github.com/RustAudio/vst-rs/commit/9eb1bef1826db1581b4162081de05c1090935afb))
|
||||
- Various doc fixes ([#177](https://github.com/RustAudio/vst-rs/pull/177))
|
||||
|
||||
### Added
|
||||
|
||||
- `begin_edit` and `end_edit` now in `Host` trait ([#151](https://github.com/RustAudio/vst-rs/pull/151))
|
||||
- Added a `prelude` for commonly used items when constructing a `Plugin` ([#161](https://github.com/RustAudio/vst-rs/pull/161))
|
||||
- Various useful implementations for `AtomicFloat` ([#150](https://github.com/RustAudio/vst-rs/pull/150))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Major breaking change:** New `Plugin` `Send` requirement ([#140](https://github.com/RustAudio/vst-rs/pull/140))
|
||||
- No longer require `Plugin` to implement `Default` ([#154](https://github.com/RustAudio/vst-rs/pull/154))
|
||||
- `impl_clicke` replaced with `num_enum` ([#168](https://github.com/RustAudio/vst-rs/pull/168))
|
||||
- Reworked `SendEventBuffer` to make it useable in `Plugin::process_events` ([#160](https://github.com/RustAudio/vst-rs/pull/160))
|
||||
- Updated dependencies and removed development dependency on `time` ([#179](https://github.com/RustAudio/vst-rs/pull/179))
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Introduced zero-valued `EventType` variant to enable zero-initialization of `Event`, fixing a panic on Rust 1.48 and newer ([#138](https://github.com/RustAudio/vst-rs/pull/138))
|
||||
- `EditorGetRect` opcode returns `1` on success, ensuring that the provided dimensions are applied by the host ([#115](https://github.com/RustAudio/vst-rs/pull/115))
|
||||
|
||||
### Added
|
||||
|
||||
- Added `update_display()` method to `Host`, telling the host to update its display (after a parameter change) via the `UpdateDisplay` opcode ([#126](https://github.com/RustAudio/vst-rs/pull/126))
|
||||
- Allow plug-in to return a custom value in `can_do()` via the `Supported::Custom` enum variant ([#130](https://github.com/RustAudio/vst-rs/pull/130))
|
||||
- Added `PartialEq` and `Eq` for `Supported` ([#135](https://github.com/RustAudio/vst-rs/pull/135))
|
||||
- Implemented `get_editor()` and `Editor` interface for `PluginInstance` to enable editor support on the host side ([#136](https://github.com/RustAudio/vst-rs/pull/136))
|
||||
- Default value (`0.0`) for `AtomicFloat` ([#139](https://github.com/RustAudio/vst-rs/pull/139))
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Changed
|
||||
|
||||
- **Major breaking change:** Restructured `Plugin` API to make it thread safe ([#65](https://github.com/RustAudio/vst-rs/pull/65))
|
||||
- Fixed a number of unsoundness issues in the `Outputs` API ([#67](https://github.com/RustAudio/vst-rs/pull/67), [#108](https://github.com/RustAudio/vst-rs/pull/108))
|
||||
- Set parameters to be automatable by default ([#99](https://github.com/RustAudio/vst-rs/pull/99))
|
||||
- Moved repository to the [RustAudio](https://github.com/RustAudio) organization and renamed it to `vst-rs` ([#90](https://github.com/RustAudio/vst-rs/pull/90), [#94](https://github.com/RustAudio/vst-rs/pull/94))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a use-after-move bug in the event iterator ([#93](https://github.com/RustAudio/vst-rs/pull/93), [#111](https://github.com/RustAudio/vst-rs/pull/111))
|
||||
|
||||
### Added
|
||||
|
||||
- Handle `Opcode::GetEffectName` to resolve name display issues on some hosts ([#89](https://github.com/RustAudio/vst-rs/pull/89))
|
||||
- More examples ([#65](https://github.com/RustAudio/vst-rs/pull/65), [#92](https://github.com/RustAudio/vst-rs/pull/92))
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added initial changelog
|
||||
- Initial project files
|
||||
|
||||
### Removed
|
||||
|
||||
- The `#[derive(Copy, Clone)]` attribute from `Outputs`.
|
||||
|
||||
### Changed
|
||||
- The signature of the `Outputs::split_at_mut` now takes an `self` parameter instead of `&mut self`.
|
||||
So calling `split_at_mut` will now move instead of "borrow".
|
||||
- Now `&mut Outputs` (instead of `Outputs`) implements the `IntoIterator` trait.
|
||||
- The return type of the `AudioBuffer::zip()` method (but it still implements the Iterator trait).
|
75
plugin/vst/Cargo.toml
Normal file
75
plugin/vst/Cargo.toml
Normal file
|
@ -0,0 +1,75 @@
|
|||
[package]
|
||||
name = "vst"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Marko Mijalkovic <marko.mijalkovic97@gmail.com>",
|
||||
"Boscop",
|
||||
"Alex Zywicki <alexander.zywicki@gmail.com>",
|
||||
"doomy <notdoomy@protonmail.com>",
|
||||
"Ms2ger",
|
||||
"Rob Saunders",
|
||||
"David Lu",
|
||||
"Aske Simon Christensen",
|
||||
"kykc",
|
||||
"Jordan Earls",
|
||||
"xnor104",
|
||||
"Nathaniel Theis",
|
||||
"Colin Wallace",
|
||||
"Henrik Nordvik",
|
||||
"Charles Saracco",
|
||||
"Frederik Halkjær" ]
|
||||
|
||||
description = "VST 2.4 API implementation in rust. Create plugins or hosts."
|
||||
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rustaudio/vst-rs"
|
||||
|
||||
license = "MIT"
|
||||
keywords = ["vst", "vst2", "plugin"]
|
||||
|
||||
autoexamples = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
disable_deprecation_warning = []
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
num-traits = "0.2"
|
||||
libc = "0.2"
|
||||
bitflags = "1"
|
||||
libloading = "0.7"
|
||||
num_enum = "0.5.2"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
[[example]]
|
||||
name = "dimension_expander"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "simple_host"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[[example]]
|
||||
name = "sine_synth"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "fwd_midi"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "gain_effect"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "transfer_and_smooth"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "ladder_filter"
|
||||
crate-type = ["cdylib"]
|
||||
|
21
plugin/vst/LICENSE
Normal file
21
plugin/vst/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Marko Mijalkovic
|
||||
|
||||
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.
|
112
plugin/vst/README.md
Normal file
112
plugin/vst/README.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
# vst-rs
|
||||
[![crates.io][crates-img]][crates-url]
|
||||
[](https://deps.rs/repo/github/rustaudio/vst-rs)
|
||||
[![Discord Chat][discord-img]][discord-url]
|
||||
[![Discourse topics][dc-img]][dc-url]
|
||||
|
||||
> **Notice**: `vst-rs` is deprecated.
|
||||
>
|
||||
> This crate is no longer actively developed or maintained. VST 2 has been [officially discontinued](http://web.archive.org/web/20210727141622/https://www.steinberg.net/en/newsandevents/news/newsdetail/article/vst-2-coming-to-an-end-4727.html) and it is [no longer possible](https://forum.juce.com/t/steinberg-closing-down-vst2-for-good/27722/25) to acquire a license to distribute VST 2 products. It is highly recommended that you make use of other libraries for developing audio plugins and plugin hosts in Rust.
|
||||
>
|
||||
> If you're looking for a high-level, multi-format framework for developing plugins in Rust, consider using [NIH-plug](https://github.com/robbert-vdh/nih-plug/) or [`baseplug`](https://github.com/wrl/baseplug/). If you're looking for bindings to specific plugin APIs, consider using [`vst3-sys`](https://github.com/RustAudio/vst3-sys/), [`clap-sys`](https://github.com/glowcoil/clap-sys), [`lv2(-sys)`](https://github.com/RustAudio/rust-lv2), or [`auv2-sys`](https://github.com/glowcoil/auv2-sys). If, despite the above warnings, you still have a need to use the VST 2 API from Rust, consider using [`vst2-sys`](https://github.com/RustAudio/vst2-sys) or generating bindings from the original VST 2 SDK using [`bindgen`](https://github.com/rust-lang/rust-bindgen).
|
||||
|
||||
`vst-rs` is a library for creating VST2 plugins in the Rust programming language.
|
||||
|
||||
This library is a work in progress, and as such it does not yet implement all
|
||||
functionality. It can create basic VST plugins without an editor interface.
|
||||
|
||||
**Note:** If you are upgrading from a version prior to 0.2.0, you will need to update
|
||||
your plugin code to be compatible with the new, thread-safe plugin API. See the
|
||||
[`transfer_and_smooth`](examples/transfer_and_smooth.rs) example for a guide on how
|
||||
to port your plugin.
|
||||
|
||||
## Library Documentation
|
||||
|
||||
Documentation for **released** versions can be found [here](https://docs.rs/vst/).
|
||||
|
||||
Development documentation (current `master` branch) can be found [here](https://rustaudio.github.io/vst-rs/vst/).
|
||||
|
||||
## Crate
|
||||
This crate is available on [crates.io](https://crates.io/crates/vst). If you prefer the bleeding-edge, you can also
|
||||
include the crate directly from the official [Github repository](https://github.com/rustaudio/vst-rs).
|
||||
|
||||
```toml
|
||||
# get from crates.io.
|
||||
vst = "0.3"
|
||||
```
|
||||
```toml
|
||||
# get directly from Github. This might be unstable!
|
||||
vst = { git = "https://github.com/rustaudio/vst-rs" }
|
||||
```
|
||||
|
||||
## Usage
|
||||
To create a plugin, simply create a type which implements the `Plugin` trait. Then call the `plugin_main` macro, which will export the necessary functions and handle dealing with the rest of the API.
|
||||
|
||||
## Example Plugin
|
||||
A simple plugin that bears no functionality. The provided `Cargo.toml` has a
|
||||
`crate-type` directive which builds a dynamic library, usable by any VST host.
|
||||
|
||||
`src/lib.rs`
|
||||
|
||||
```rust
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use vst::prelude::*;
|
||||
|
||||
struct BasicPlugin;
|
||||
|
||||
impl Plugin for BasicPlugin {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
BasicPlugin
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "Basic Plugin".to_string(),
|
||||
unique_id: 1357, // Used by hosts to differentiate between plugins.
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(BasicPlugin); // Important!
|
||||
```
|
||||
|
||||
`Cargo.toml`
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "basic_vst"
|
||||
version = "0.0.1"
|
||||
authors = ["Author <author@example.com>"]
|
||||
|
||||
[dependencies]
|
||||
vst = { git = "https://github.com/rustaudio/vst-rs" }
|
||||
|
||||
[lib]
|
||||
name = "basicvst"
|
||||
crate-type = ["cdylib"]
|
||||
```
|
||||
|
||||
[crates-img]: https://img.shields.io/crates/v/vst.svg
|
||||
[crates-url]: https://crates.io/crates/vst
|
||||
[discord-img]: https://img.shields.io/discord/590254806208217089.svg?label=Discord&logo=discord&color=blue
|
||||
[discord-url]: https://discord.gg/QPdhk2u
|
||||
[dc-img]: https://img.shields.io/discourse/https/rust-audio.discourse.group/topics.svg?logo=discourse&color=blue
|
||||
[dc-url]: https://rust-audio.discourse.group
|
||||
|
||||
#### Packaging on OS X
|
||||
|
||||
On OS X VST plugins are packaged inside loadable bundles.
|
||||
To package your VST as a loadable bundle you may use the `osx_vst_bundler.sh` script this library provides.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
./osx_vst_bundler.sh Plugin target/release/plugin.dylib
|
||||
Creates a Plugin.vst bundle
|
||||
```
|
||||
|
||||
## Special Thanks
|
||||
[Marko Mijalkovic](https://github.com/overdrivenpotato) for [initiating this project](https://github.com/overdrivenpotato/rust-vst2)
|
222
plugin/vst/examples/dimension_expander.rs
Normal file
222
plugin/vst/examples/dimension_expander.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
// author: Marko Mijalkovic <marko.mijalkovic97@gmail.com>
|
||||
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use vst::prelude::*;
|
||||
|
||||
/// Calculate the length in samples for a delay. Size ranges from 0.0 to 1.0.
|
||||
fn delay(index: usize, mut size: f32) -> isize {
|
||||
const SIZE_OFFSET: f32 = 0.06;
|
||||
const SIZE_MULT: f32 = 1_000.0;
|
||||
|
||||
size += SIZE_OFFSET;
|
||||
|
||||
// Spread ratio between delays
|
||||
const SPREAD: f32 = 0.3;
|
||||
|
||||
let base = size * SIZE_MULT;
|
||||
let mult = (index as f32 * SPREAD) + 1.0;
|
||||
let offset = if index > 2 { base * SPREAD / 2.0 } else { 0.0 };
|
||||
|
||||
(base * mult + offset) as isize
|
||||
}
|
||||
|
||||
/// A left channel and right channel sample.
|
||||
type SamplePair = (f32, f32);
|
||||
|
||||
/// The Dimension Expander.
|
||||
struct DimensionExpander {
|
||||
buffers: Vec<VecDeque<SamplePair>>,
|
||||
params: Arc<DimensionExpanderParameters>,
|
||||
old_size: f32,
|
||||
}
|
||||
|
||||
struct DimensionExpanderParameters {
|
||||
dry_wet: AtomicFloat,
|
||||
size: AtomicFloat,
|
||||
}
|
||||
|
||||
impl DimensionExpander {
|
||||
fn new(size: f32, dry_wet: f32) -> DimensionExpander {
|
||||
const NUM_DELAYS: usize = 4;
|
||||
|
||||
let mut buffers = Vec::new();
|
||||
|
||||
// Generate delay buffers
|
||||
for i in 0..NUM_DELAYS {
|
||||
let samples = delay(i, size);
|
||||
let mut buffer = VecDeque::with_capacity(samples as usize);
|
||||
|
||||
// Fill in the delay buffers with empty samples
|
||||
for _ in 0..samples {
|
||||
buffer.push_back((0.0, 0.0));
|
||||
}
|
||||
|
||||
buffers.push(buffer);
|
||||
}
|
||||
|
||||
DimensionExpander {
|
||||
buffers,
|
||||
params: Arc::new(DimensionExpanderParameters {
|
||||
dry_wet: AtomicFloat::new(dry_wet),
|
||||
size: AtomicFloat::new(size),
|
||||
}),
|
||||
old_size: size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the delay buffers with a new size value.
|
||||
fn resize(&mut self, n: f32) {
|
||||
let old_size = self.old_size;
|
||||
|
||||
for (i, buffer) in self.buffers.iter_mut().enumerate() {
|
||||
// Calculate the size difference between delays
|
||||
let old_delay = delay(i, old_size);
|
||||
let new_delay = delay(i, n);
|
||||
|
||||
let diff = new_delay - old_delay;
|
||||
|
||||
// Add empty samples if the delay was increased, remove if decreased
|
||||
if diff > 0 {
|
||||
for _ in 0..diff {
|
||||
buffer.push_back((0.0, 0.0));
|
||||
}
|
||||
} else if diff < 0 {
|
||||
for _ in 0..-diff {
|
||||
let _ = buffer.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.old_size = n;
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DimensionExpander {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
DimensionExpander::new(0.12, 0.66)
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "Dimension Expander".to_string(),
|
||||
vendor: "overdrivenpotato".to_string(),
|
||||
unique_id: 243723071,
|
||||
version: 1,
|
||||
inputs: 2,
|
||||
outputs: 2,
|
||||
parameters: 2,
|
||||
category: Category::Effect,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
let (inputs, outputs) = buffer.split();
|
||||
|
||||
// Assume 2 channels
|
||||
if inputs.len() < 2 || outputs.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resize if size changed
|
||||
let size = self.params.size.get();
|
||||
if size != self.old_size {
|
||||
self.resize(size);
|
||||
}
|
||||
|
||||
// Iterate over inputs as (&f32, &f32)
|
||||
let (l, r) = inputs.split_at(1);
|
||||
let stereo_in = l[0].iter().zip(r[0].iter());
|
||||
|
||||
// Iterate over outputs as (&mut f32, &mut f32)
|
||||
let (mut l, mut r) = outputs.split_at_mut(1);
|
||||
let stereo_out = l[0].iter_mut().zip(r[0].iter_mut());
|
||||
|
||||
// Zip and process
|
||||
for ((left_in, right_in), (left_out, right_out)) in stereo_in.zip(stereo_out) {
|
||||
// Push the new samples into the delay buffers.
|
||||
for buffer in &mut self.buffers {
|
||||
buffer.push_back((*left_in, *right_in));
|
||||
}
|
||||
|
||||
let mut left_processed = 0.0;
|
||||
let mut right_processed = 0.0;
|
||||
|
||||
// Recalculate time per sample
|
||||
let time_s = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64();
|
||||
|
||||
// Use buffer index to offset volume LFO
|
||||
for (n, buffer) in self.buffers.iter_mut().enumerate() {
|
||||
if let Some((left_old, right_old)) = buffer.pop_front() {
|
||||
const LFO_FREQ: f64 = 0.5;
|
||||
const WET_MULT: f32 = 0.66;
|
||||
|
||||
let offset = 0.25 * (n % 4) as f64;
|
||||
|
||||
// Sine wave volume LFO
|
||||
let lfo = ((time_s * LFO_FREQ + offset) * PI * 2.0).sin() as f32;
|
||||
|
||||
let wet = self.params.dry_wet.get() * WET_MULT;
|
||||
let mono = (left_old + right_old) / 2.0;
|
||||
|
||||
// Flip right channel and keep left mono so that the result is
|
||||
// entirely stereo
|
||||
left_processed += mono * wet * lfo;
|
||||
right_processed += -mono * wet * lfo;
|
||||
}
|
||||
}
|
||||
|
||||
// By only adding to the input, the output value always remains the same in mono
|
||||
*left_out = *left_in + left_processed;
|
||||
*right_out = *right_in + right_processed;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginParameters for DimensionExpanderParameters {
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
match index {
|
||||
0 => self.size.get(),
|
||||
1 => self.dry_wet.get(),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_text(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => format!("{}", (self.size.get() * 1000.0) as isize),
|
||||
1 => format!("{:.1}%", self.dry_wet.get() * 100.0),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_name(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => "Size",
|
||||
1 => "Dry/Wet",
|
||||
_ => "",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn set_parameter(&self, index: i32, val: f32) {
|
||||
match index {
|
||||
0 => self.size.set(val),
|
||||
1 => self.dry_wet.set(val),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(DimensionExpander);
|
71
plugin/vst/examples/fwd_midi.rs
Normal file
71
plugin/vst/examples/fwd_midi.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use vst::api;
|
||||
use vst::prelude::*;
|
||||
|
||||
plugin_main!(MyPlugin); // Important!
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyPlugin {
|
||||
host: HostCallback,
|
||||
recv_buffer: SendEventBuffer,
|
||||
send_buffer: SendEventBuffer,
|
||||
}
|
||||
|
||||
impl MyPlugin {
|
||||
fn send_midi(&mut self) {
|
||||
self.send_buffer
|
||||
.send_events(self.recv_buffer.events().events(), &mut self.host);
|
||||
self.recv_buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for MyPlugin {
|
||||
fn new(host: HostCallback) -> Self {
|
||||
MyPlugin {
|
||||
host,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "fwd_midi".to_string(),
|
||||
unique_id: 7357001, // Used by hosts to differentiate between plugins.
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn process_events(&mut self, events: &api::Events) {
|
||||
self.recv_buffer.store_events(events.events());
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
for (input, output) in buffer.zip() {
|
||||
for (in_sample, out_sample) in input.iter().zip(output) {
|
||||
*out_sample = *in_sample;
|
||||
}
|
||||
}
|
||||
self.send_midi();
|
||||
}
|
||||
|
||||
fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
|
||||
for (input, output) in buffer.zip() {
|
||||
for (in_sample, out_sample) in input.iter().zip(output) {
|
||||
*out_sample = *in_sample;
|
||||
}
|
||||
}
|
||||
self.send_midi();
|
||||
}
|
||||
|
||||
fn can_do(&self, can_do: CanDo) -> vst::api::Supported {
|
||||
use vst::api::Supported::*;
|
||||
use vst::plugin::CanDo::*;
|
||||
|
||||
match can_do {
|
||||
SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent => Yes,
|
||||
_ => No,
|
||||
}
|
||||
}
|
||||
}
|
129
plugin/vst/examples/gain_effect.rs
Normal file
129
plugin/vst/examples/gain_effect.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
// author: doomy <notdoomy@protonmail.com>
|
||||
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vst::prelude::*;
|
||||
|
||||
/// Simple Gain Effect.
|
||||
/// Note that this does not use a proper scale for sound and shouldn't be used in
|
||||
/// a production amplification effect! This is purely for demonstration purposes,
|
||||
/// as well as to keep things simple as this is meant to be a starting point for
|
||||
/// any effect.
|
||||
struct GainEffect {
|
||||
// Store a handle to the plugin's parameter object.
|
||||
params: Arc<GainEffectParameters>,
|
||||
}
|
||||
|
||||
/// The plugin's parameter object contains the values of parameters that can be
|
||||
/// adjusted from the host. If we were creating an effect that didn't allow the
|
||||
/// user to modify it at runtime or have any controls, we could omit this part.
|
||||
///
|
||||
/// The parameters object is shared between the processing and GUI threads.
|
||||
/// For this reason, all mutable state in the object has to be represented
|
||||
/// through thread-safe interior mutability. The easiest way to achieve this
|
||||
/// is to store the parameters in atomic containers.
|
||||
struct GainEffectParameters {
|
||||
// The plugin's state consists of a single parameter: amplitude.
|
||||
amplitude: AtomicFloat,
|
||||
}
|
||||
|
||||
impl Default for GainEffectParameters {
|
||||
fn default() -> GainEffectParameters {
|
||||
GainEffectParameters {
|
||||
amplitude: AtomicFloat::new(0.5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All plugins using `vst` also need to implement the `Plugin` trait. Here, we
|
||||
// define functions that give necessary info to our host.
|
||||
impl Plugin for GainEffect {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
// Note that controls will always return a value from 0 - 1.
|
||||
// Setting a default to 0.5 means it's halfway up.
|
||||
GainEffect {
|
||||
params: Arc::new(GainEffectParameters::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "Gain Effect in Rust".to_string(),
|
||||
vendor: "Rust DSP".to_string(),
|
||||
unique_id: 243723072,
|
||||
version: 1,
|
||||
inputs: 2,
|
||||
outputs: 2,
|
||||
// This `parameters` bit is important; without it, none of our
|
||||
// parameters will be shown!
|
||||
parameters: 1,
|
||||
category: Category::Effect,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Here is where the bulk of our audio processing code goes.
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
// Read the amplitude from the parameter object
|
||||
let amplitude = self.params.amplitude.get();
|
||||
// First, we destructure our audio buffer into an arbitrary number of
|
||||
// input and output buffers. Usually, we'll be dealing with stereo (2 of each)
|
||||
// but that might change.
|
||||
for (input_buffer, output_buffer) in buffer.zip() {
|
||||
// Next, we'll loop through each individual sample so we can apply the amplitude
|
||||
// value to it.
|
||||
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
|
||||
*output_sample = *input_sample * amplitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the parameter object. This method can be omitted if the
|
||||
// plugin has no parameters.
|
||||
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginParameters for GainEffectParameters {
|
||||
// the `get_parameter` function reads the value of a parameter.
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
match index {
|
||||
0 => self.amplitude.get(),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// the `set_parameter` function sets the value of a parameter.
|
||||
fn set_parameter(&self, index: i32, val: f32) {
|
||||
#[allow(clippy::single_match)]
|
||||
match index {
|
||||
0 => self.amplitude.set(val),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// This is what will display underneath our control. We can
|
||||
// format it into a string that makes the most since.
|
||||
fn get_parameter_text(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => format!("{:.2}", (self.amplitude.get() - 0.5) * 2f32),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// This shows the control's name.
|
||||
fn get_parameter_name(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => "Amplitude",
|
||||
_ => "",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// This part is important! Without it, our plugin won't work.
|
||||
plugin_main!(GainEffect);
|
248
plugin/vst/examples/ladder_filter.rs
Normal file
248
plugin/vst/examples/ladder_filter.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
//! This zero-delay feedback filter is based on a 4-stage transistor ladder filter.
|
||||
//! It follows the following equations:
|
||||
//! x = input - tanh(self.res * self.vout[3])
|
||||
//! vout[0] = self.params.g.get() * (tanh(x) - tanh(self.vout[0])) + self.s[0]
|
||||
//! vout[1] = self.params.g.get() * (tanh(self.vout[0]) - tanh(self.vout[1])) + self.s[1]
|
||||
//! vout[0] = self.params.g.get() * (tanh(self.vout[1]) - tanh(self.vout[2])) + self.s[2]
|
||||
//! vout[0] = self.params.g.get() * (tanh(self.vout[2]) - tanh(self.vout[3])) + self.s[3]
|
||||
//! since we can't easily solve a nonlinear equation,
|
||||
//! Mystran's fixed-pivot method is used to approximate the tanh() parts.
|
||||
//! Quality can be improved a lot by oversampling a bit.
|
||||
//! Feedback is clipped independently of the input, so it doesn't disappear at high gains.
|
||||
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
use std::f32::consts::PI;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use vst::prelude::*;
|
||||
|
||||
// this is a 4-pole filter with resonance, which is why there's 4 states and vouts
|
||||
#[derive(Clone)]
|
||||
struct LadderFilter {
|
||||
// Store a handle to the plugin's parameter object.
|
||||
params: Arc<LadderParameters>,
|
||||
// the output of the different filter stages
|
||||
vout: [f32; 4],
|
||||
// s is the "state" parameter. In an IIR it would be the last value from the filter
|
||||
// In this we find it by trapezoidal integration to avoid the unit delay
|
||||
s: [f32; 4],
|
||||
}
|
||||
|
||||
struct LadderParameters {
|
||||
// the "cutoff" parameter. Determines how heavy filtering is
|
||||
cutoff: AtomicFloat,
|
||||
g: AtomicFloat,
|
||||
// needed to calculate cutoff.
|
||||
sample_rate: AtomicFloat,
|
||||
// makes a peak at cutoff
|
||||
res: AtomicFloat,
|
||||
// used to choose where we want our output to be
|
||||
poles: AtomicUsize,
|
||||
// pole_value is just to be able to use get_parameter on poles
|
||||
pole_value: AtomicFloat,
|
||||
// a drive parameter. Just used to increase the volume, which results in heavier distortion
|
||||
drive: AtomicFloat,
|
||||
}
|
||||
|
||||
impl Default for LadderParameters {
|
||||
fn default() -> LadderParameters {
|
||||
LadderParameters {
|
||||
cutoff: AtomicFloat::new(1000.),
|
||||
res: AtomicFloat::new(2.),
|
||||
poles: AtomicUsize::new(3),
|
||||
pole_value: AtomicFloat::new(1.),
|
||||
drive: AtomicFloat::new(0.),
|
||||
sample_rate: AtomicFloat::new(44100.),
|
||||
g: AtomicFloat::new(0.07135868),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// member methods for the struct
|
||||
impl LadderFilter {
|
||||
// the state needs to be updated after each process. Found by trapezoidal integration
|
||||
fn update_state(&mut self) {
|
||||
self.s[0] = 2. * self.vout[0] - self.s[0];
|
||||
self.s[1] = 2. * self.vout[1] - self.s[1];
|
||||
self.s[2] = 2. * self.vout[2] - self.s[2];
|
||||
self.s[3] = 2. * self.vout[3] - self.s[3];
|
||||
}
|
||||
|
||||
// performs a complete filter process (mystran's method)
|
||||
fn tick_pivotal(&mut self, input: f32) {
|
||||
if self.params.drive.get() > 0. {
|
||||
self.run_ladder_nonlinear(input * (self.params.drive.get() + 0.7));
|
||||
} else {
|
||||
//
|
||||
self.run_ladder_linear(input);
|
||||
}
|
||||
self.update_state();
|
||||
}
|
||||
|
||||
// nonlinear ladder filter function with distortion.
|
||||
fn run_ladder_nonlinear(&mut self, input: f32) {
|
||||
let mut a = [1f32; 5];
|
||||
let base = [input, self.s[0], self.s[1], self.s[2], self.s[3]];
|
||||
// a[n] is the fixed-pivot approximation for tanh()
|
||||
for n in 0..base.len() {
|
||||
if base[n] != 0. {
|
||||
a[n] = base[n].tanh() / base[n];
|
||||
} else {
|
||||
a[n] = 1.;
|
||||
}
|
||||
}
|
||||
// denominators of solutions of individual stages. Simplifies the math a bit
|
||||
let g0 = 1. / (1. + self.params.g.get() * a[1]);
|
||||
let g1 = 1. / (1. + self.params.g.get() * a[2]);
|
||||
let g2 = 1. / (1. + self.params.g.get() * a[3]);
|
||||
let g3 = 1. / (1. + self.params.g.get() * a[4]);
|
||||
// these are just factored out of the feedback solution. Makes the math way easier to read
|
||||
let f3 = self.params.g.get() * a[3] * g3;
|
||||
let f2 = self.params.g.get() * a[2] * g2 * f3;
|
||||
let f1 = self.params.g.get() * a[1] * g1 * f2;
|
||||
let f0 = self.params.g.get() * g0 * f1;
|
||||
// outputs a 24db filter
|
||||
self.vout[3] =
|
||||
(f0 * input * a[0] + f1 * g0 * self.s[0] + f2 * g1 * self.s[1] + f3 * g2 * self.s[2] + g3 * self.s[3])
|
||||
/ (f0 * self.params.res.get() * a[3] + 1.);
|
||||
// since we know the feedback, we can solve the remaining outputs:
|
||||
self.vout[0] = g0
|
||||
* (self.params.g.get() * a[1] * (input * a[0] - self.params.res.get() * a[3] * self.vout[3]) + self.s[0]);
|
||||
self.vout[1] = g1 * (self.params.g.get() * a[2] * self.vout[0] + self.s[1]);
|
||||
self.vout[2] = g2 * (self.params.g.get() * a[3] * self.vout[1] + self.s[2]);
|
||||
}
|
||||
|
||||
// linear version without distortion
|
||||
pub fn run_ladder_linear(&mut self, input: f32) {
|
||||
// denominators of solutions of individual stages. Simplifies the math a bit
|
||||
let g0 = 1. / (1. + self.params.g.get());
|
||||
let g1 = self.params.g.get() * g0 * g0;
|
||||
let g2 = self.params.g.get() * g1 * g0;
|
||||
let g3 = self.params.g.get() * g2 * g0;
|
||||
// outputs a 24db filter
|
||||
self.vout[3] =
|
||||
(g3 * self.params.g.get() * input + g0 * self.s[3] + g1 * self.s[2] + g2 * self.s[1] + g3 * self.s[0])
|
||||
/ (g3 * self.params.g.get() * self.params.res.get() + 1.);
|
||||
// since we know the feedback, we can solve the remaining outputs:
|
||||
self.vout[0] = g0 * (self.params.g.get() * (input - self.params.res.get() * self.vout[3]) + self.s[0]);
|
||||
self.vout[1] = g0 * (self.params.g.get() * self.vout[0] + self.s[1]);
|
||||
self.vout[2] = g0 * (self.params.g.get() * self.vout[1] + self.s[2]);
|
||||
}
|
||||
}
|
||||
|
||||
impl LadderParameters {
|
||||
pub fn set_cutoff(&self, value: f32) {
|
||||
// cutoff formula gives us a natural feeling cutoff knob that spends more time in the low frequencies
|
||||
self.cutoff.set(20000. * (1.8f32.powf(10. * value - 10.)));
|
||||
// bilinear transformation for g gives us a very accurate cutoff
|
||||
self.g.set((PI * self.cutoff.get() / (self.sample_rate.get())).tan());
|
||||
}
|
||||
|
||||
// returns the value used to set cutoff. for get_parameter function
|
||||
pub fn get_cutoff(&self) -> f32 {
|
||||
1. + 0.17012975 * (0.00005 * self.cutoff.get()).ln()
|
||||
}
|
||||
|
||||
pub fn set_poles(&self, value: f32) {
|
||||
self.pole_value.set(value);
|
||||
self.poles.store(((value * 3.).round()) as usize, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginParameters for LadderParameters {
|
||||
// get_parameter has to return the value used in set_parameter
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
match index {
|
||||
0 => self.get_cutoff(),
|
||||
1 => self.res.get() / 4.,
|
||||
2 => self.pole_value.get(),
|
||||
3 => self.drive.get() / 5.,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&self, index: i32, value: f32) {
|
||||
match index {
|
||||
0 => self.set_cutoff(value),
|
||||
1 => self.res.set(value * 4.),
|
||||
2 => self.set_poles(value),
|
||||
3 => self.drive.set(value * 5.),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_name(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => "cutoff".to_string(),
|
||||
1 => "resonance".to_string(),
|
||||
2 => "filter order".to_string(),
|
||||
3 => "drive".to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_label(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => "Hz".to_string(),
|
||||
1 => "%".to_string(),
|
||||
2 => "poles".to_string(),
|
||||
3 => "%".to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
// This is what will display underneath our control. We can
|
||||
// format it into a string that makes the most sense.
|
||||
fn get_parameter_text(&self, index: i32) -> String {
|
||||
match index {
|
||||
0 => format!("{:.0}", self.cutoff.get()),
|
||||
1 => format!("{:.3}", self.res.get()),
|
||||
2 => format!("{}", self.poles.load(Ordering::Relaxed) + 1),
|
||||
3 => format!("{:.3}", self.drive.get()),
|
||||
_ => format!(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for LadderFilter {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
LadderFilter {
|
||||
vout: [0f32; 4],
|
||||
s: [0f32; 4],
|
||||
params: Arc::new(LadderParameters::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sample_rate(&mut self, rate: f32) {
|
||||
self.params.sample_rate.set(rate);
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "LadderFilter".to_string(),
|
||||
unique_id: 9263,
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
category: Category::Effect,
|
||||
parameters: 4,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
for (input_buffer, output_buffer) in buffer.zip() {
|
||||
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
|
||||
self.tick_pivotal(*input_sample);
|
||||
// the poles parameter chooses which filter stage we take our output from.
|
||||
*output_sample = self.vout[self.params.poles.load(Ordering::Relaxed)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(LadderFilter);
|
63
plugin/vst/examples/simple_host.rs
Normal file
63
plugin/vst/examples/simple_host.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
extern crate vst;
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use vst::host::{Host, PluginLoader};
|
||||
use vst::plugin::Plugin;
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct SampleHost;
|
||||
|
||||
impl Host for SampleHost {
|
||||
fn automate(&self, index: i32, value: f32) {
|
||||
println!("Parameter {} had its value changed to {}", index, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
println!("usage: simple_host path/to/vst");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let path = Path::new(&args[1]);
|
||||
|
||||
// Create the host
|
||||
let host = Arc::new(Mutex::new(SampleHost));
|
||||
|
||||
println!("Loading {}...", path.to_str().unwrap());
|
||||
|
||||
// Load the plugin
|
||||
let mut loader =
|
||||
PluginLoader::load(path, Arc::clone(&host)).unwrap_or_else(|e| panic!("Failed to load plugin: {}", e));
|
||||
|
||||
// Create an instance of the plugin
|
||||
let mut instance = loader.instance().unwrap();
|
||||
|
||||
// Get the plugin information
|
||||
let info = instance.get_info();
|
||||
|
||||
println!(
|
||||
"Loaded '{}':\n\t\
|
||||
Vendor: {}\n\t\
|
||||
Presets: {}\n\t\
|
||||
Parameters: {}\n\t\
|
||||
VST ID: {}\n\t\
|
||||
Version: {}\n\t\
|
||||
Initial Delay: {} samples",
|
||||
info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay
|
||||
);
|
||||
|
||||
// Initialize the instance
|
||||
instance.init();
|
||||
println!("Initialized instance!");
|
||||
|
||||
println!("Closing instance...");
|
||||
// Close the instance. This is not necessary as the instance is shut down when
|
||||
// it is dropped as it goes out of scope.
|
||||
// drop(instance);
|
||||
}
|
160
plugin/vst/examples/sine_synth.rs
Normal file
160
plugin/vst/examples/sine_synth.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
// author: Rob Saunders <hello@robsaunders.io>
|
||||
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use vst::prelude::*;
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Convert the midi note's pitch into the equivalent frequency.
|
||||
///
|
||||
/// This function assumes A4 is 440hz.
|
||||
fn midi_pitch_to_freq(pitch: u8) -> f64 {
|
||||
const A4_PITCH: i8 = 69;
|
||||
const A4_FREQ: f64 = 440.0;
|
||||
|
||||
// Midi notes can be 0-127
|
||||
((f64::from(pitch as i8 - A4_PITCH)) / 12.).exp2() * A4_FREQ
|
||||
}
|
||||
|
||||
struct SineSynth {
|
||||
sample_rate: f64,
|
||||
time: f64,
|
||||
note_duration: f64,
|
||||
note: Option<u8>,
|
||||
}
|
||||
|
||||
impl SineSynth {
|
||||
fn time_per_sample(&self) -> f64 {
|
||||
1.0 / self.sample_rate
|
||||
}
|
||||
|
||||
/// Process an incoming midi event.
|
||||
///
|
||||
/// The midi data is split up like so:
|
||||
///
|
||||
/// `data[0]`: Contains the status and the channel. Source: [source]
|
||||
/// `data[1]`: Contains the supplemental data for the message - so, if this was a NoteOn then
|
||||
/// this would contain the note.
|
||||
/// `data[2]`: Further supplemental data. Would be velocity in the case of a NoteOn message.
|
||||
///
|
||||
/// [source]: http://www.midimountain.com/midi/midi_status.htm
|
||||
fn process_midi_event(&mut self, data: [u8; 3]) {
|
||||
match data[0] {
|
||||
128 => self.note_off(data[1]),
|
||||
144 => self.note_on(data[1]),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn note_on(&mut self, note: u8) {
|
||||
self.note_duration = 0.0;
|
||||
self.note = Some(note)
|
||||
}
|
||||
|
||||
fn note_off(&mut self, note: u8) {
|
||||
if self.note == Some(note) {
|
||||
self.note = None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const TAU: f64 = PI * 2.0;
|
||||
|
||||
impl Plugin for SineSynth {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
SineSynth {
|
||||
sample_rate: 44100.0,
|
||||
note_duration: 0.0,
|
||||
time: 0.0,
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "SineSynth".to_string(),
|
||||
vendor: "DeathDisco".to_string(),
|
||||
unique_id: 6667,
|
||||
category: Category::Synth,
|
||||
inputs: 2,
|
||||
outputs: 2,
|
||||
parameters: 0,
|
||||
initial_delay: 0,
|
||||
..Info::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[allow(clippy::single_match)]
|
||||
fn process_events(&mut self, events: &Events) {
|
||||
for event in events.events() {
|
||||
match event {
|
||||
Event::Midi(ev) => self.process_midi_event(ev.data),
|
||||
// More events can be handled here.
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sample_rate(&mut self, rate: f32) {
|
||||
self.sample_rate = f64::from(rate);
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
let samples = buffer.samples();
|
||||
let (_, mut outputs) = buffer.split();
|
||||
let output_count = outputs.len();
|
||||
let per_sample = self.time_per_sample();
|
||||
let mut output_sample;
|
||||
for sample_idx in 0..samples {
|
||||
let time = self.time;
|
||||
let note_duration = self.note_duration;
|
||||
if let Some(current_note) = self.note {
|
||||
let signal = (time * midi_pitch_to_freq(current_note) * TAU).sin();
|
||||
|
||||
// Apply a quick envelope to the attack of the signal to avoid popping.
|
||||
let attack = 0.5;
|
||||
let alpha = if note_duration < attack {
|
||||
note_duration / attack
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
output_sample = (signal * alpha) as f32;
|
||||
|
||||
self.time += per_sample;
|
||||
self.note_duration += per_sample;
|
||||
} else {
|
||||
output_sample = 0.0;
|
||||
}
|
||||
for buf_idx in 0..output_count {
|
||||
let buff = outputs.get_mut(buf_idx);
|
||||
buff[sample_idx] = output_sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_do(&self, can_do: CanDo) -> Supported {
|
||||
match can_do {
|
||||
CanDo::ReceiveMidiEvent => Supported::Yes,
|
||||
_ => Supported::Maybe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(SineSynth);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use midi_pitch_to_freq;
|
||||
|
||||
#[test]
|
||||
fn test_midi_pitch_to_freq() {
|
||||
for i in 0..127 {
|
||||
// expect no panics
|
||||
midi_pitch_to_freq(i);
|
||||
}
|
||||
}
|
||||
}
|
136
plugin/vst/examples/transfer_and_smooth.rs
Normal file
136
plugin/vst/examples/transfer_and_smooth.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
// This example illustrates how an existing plugin can be ported to the new,
|
||||
// thread-safe API with the help of the ParameterTransfer struct.
|
||||
// It shows how the parameter iteration feature of ParameterTransfer can be
|
||||
// used to react explicitly to parameter changes in an efficient way (here,
|
||||
// to implement smoothing of parameters).
|
||||
|
||||
#[macro_use]
|
||||
extern crate vst;
|
||||
|
||||
use std::f32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vst::prelude::*;
|
||||
|
||||
const PARAMETER_COUNT: usize = 100;
|
||||
const BASE_FREQUENCY: f32 = 5.0;
|
||||
const FILTER_FACTOR: f32 = 0.01; // Set this to 1.0 to disable smoothing.
|
||||
const TWO_PI: f32 = 2.0 * f32::consts::PI;
|
||||
|
||||
// 1. Define a struct to hold parameters. Put a ParameterTransfer inside it,
|
||||
// plus optionally a HostCallback.
|
||||
struct MyPluginParameters {
|
||||
#[allow(dead_code)]
|
||||
host: HostCallback,
|
||||
transfer: ParameterTransfer,
|
||||
}
|
||||
|
||||
// 2. Put an Arc reference to your parameter struct in your main Plugin struct.
|
||||
struct MyPlugin {
|
||||
params: Arc<MyPluginParameters>,
|
||||
states: Vec<Smoothed>,
|
||||
sample_rate: f32,
|
||||
phase: f32,
|
||||
}
|
||||
|
||||
// 3. Implement PluginParameters for your parameter struct.
|
||||
// The set_parameter and get_parameter just access the ParameterTransfer.
|
||||
// The other methods can be implemented on top of this as well.
|
||||
impl PluginParameters for MyPluginParameters {
|
||||
fn set_parameter(&self, index: i32, value: f32) {
|
||||
self.transfer.set_parameter(index as usize, value);
|
||||
}
|
||||
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
self.transfer.get_parameter(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for MyPlugin {
|
||||
fn new(host: HostCallback) -> Self {
|
||||
MyPlugin {
|
||||
// 4. Initialize your main Plugin struct with a parameter struct
|
||||
// wrapped in an Arc, and put the HostCallback inside it.
|
||||
params: Arc::new(MyPluginParameters {
|
||||
host,
|
||||
transfer: ParameterTransfer::new(PARAMETER_COUNT),
|
||||
}),
|
||||
states: vec![Smoothed::default(); PARAMETER_COUNT],
|
||||
sample_rate: 44100.0,
|
||||
phase: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
parameters: PARAMETER_COUNT as i32,
|
||||
inputs: 0,
|
||||
outputs: 2,
|
||||
category: Category::Synth,
|
||||
f64_precision: false,
|
||||
|
||||
name: "transfer_and_smooth".to_string(),
|
||||
vendor: "Loonies".to_string(),
|
||||
unique_id: 0x500007,
|
||||
version: 100,
|
||||
|
||||
..Info::default()
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return a reference to the parameter struct from get_parameter_object.
|
||||
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||
}
|
||||
|
||||
fn set_sample_rate(&mut self, sample_rate: f32) {
|
||||
self.sample_rate = sample_rate;
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
// 6. In the process method, iterate over changed parameters and do
|
||||
// for each what you would previously do in set_parameter. Since this
|
||||
// runs in the processing thread, it has mutable access to the Plugin.
|
||||
for (p, value) in self.params.transfer.iterate(true) {
|
||||
// Example: Update filter state of changed parameter.
|
||||
self.states[p].set(value);
|
||||
}
|
||||
|
||||
// Example: Dummy synth adding together a bunch of sines.
|
||||
let samples = buffer.samples();
|
||||
let mut outputs = buffer.split().1;
|
||||
for i in 0..samples {
|
||||
let mut sum = 0.0;
|
||||
for p in 0..PARAMETER_COUNT {
|
||||
let amp = self.states[p].get();
|
||||
if amp != 0.0 {
|
||||
sum += (self.phase * p as f32 * TWO_PI).sin() * amp;
|
||||
}
|
||||
}
|
||||
outputs[0][i] = sum;
|
||||
outputs[1][i] = sum;
|
||||
self.phase = (self.phase + BASE_FREQUENCY / self.sample_rate).fract();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Parameter smoothing as an example of non-trivial parameter handling
|
||||
// that has to happen when a parameter changes.
|
||||
#[derive(Clone, Default)]
|
||||
struct Smoothed {
|
||||
state: f32,
|
||||
target: f32,
|
||||
}
|
||||
|
||||
impl Smoothed {
|
||||
fn set(&mut self, value: f32) {
|
||||
self.target = value;
|
||||
}
|
||||
|
||||
fn get(&mut self) -> f32 {
|
||||
self.state += (self.target - self.state) * FILTER_FACTOR;
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(MyPlugin);
|
61
plugin/vst/osx_vst_bundler.sh
Executable file
61
plugin/vst/osx_vst_bundler.sh
Executable file
|
@ -0,0 +1,61 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Make sure we have the arguments we need
|
||||
if [[ -z $1 || -z $2 ]]; then
|
||||
echo "Generates a macOS bundle from a compiled dylib file"
|
||||
echo "Example:"
|
||||
echo -e "\t$0 Plugin target/release/plugin.dylib"
|
||||
echo -e "\tCreates a Plugin.vst bundle"
|
||||
else
|
||||
# Make the bundle folder
|
||||
mkdir -p "$1.vst/Contents/MacOS"
|
||||
|
||||
# Create the PkgInfo
|
||||
echo "BNDL????" > "$1.vst/Contents/PkgInfo"
|
||||
|
||||
#build the Info.Plist
|
||||
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
<plist version=\"1.0\">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$1</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>vst</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.rust-vst.$1</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>$1</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>$((RANDOM % 9999))</string>
|
||||
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<string></string>
|
||||
|
||||
</dict>
|
||||
</plist>" > "$1.vst/Contents/Info.plist"
|
||||
|
||||
# move the provided library to the correct location
|
||||
cp "$2" "$1.vst/Contents/MacOS/$1"
|
||||
|
||||
echo "Created bundle $1.vst"
|
||||
fi
|
1
plugin/vst/rustfmt.toml
Normal file
1
plugin/vst/rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
max_width = 120
|
927
plugin/vst/src/api.rs
Normal file
927
plugin/vst/src/api.rs
Normal file
|
@ -0,0 +1,927 @@
|
|||
//! Structures and types for interfacing with the VST 2.4 API.
|
||||
|
||||
use std::os::raw::c_void;
|
||||
use std::sync::Arc;
|
||||
|
||||
use self::consts::*;
|
||||
use crate::{
|
||||
editor::Editor,
|
||||
plugin::{Info, Plugin, PluginParameters},
|
||||
};
|
||||
|
||||
/// Constant values
|
||||
#[allow(missing_docs)] // For obvious constants
|
||||
pub mod consts {
|
||||
|
||||
pub const MAX_PRESET_NAME_LEN: usize = 24;
|
||||
pub const MAX_PARAM_STR_LEN: usize = 32;
|
||||
pub const MAX_LABEL: usize = 64;
|
||||
pub const MAX_SHORT_LABEL: usize = 8;
|
||||
pub const MAX_PRODUCT_STR_LEN: usize = 64;
|
||||
pub const MAX_VENDOR_STR_LEN: usize = 64;
|
||||
|
||||
/// VST plugins are identified by a magic number. This corresponds to 0x56737450.
|
||||
pub const VST_MAGIC: i32 = ('V' as i32) << 24 | ('s' as i32) << 16 | ('t' as i32) << 8 | ('P' as i32);
|
||||
}
|
||||
|
||||
/// `VSTPluginMain` function signature.
|
||||
pub type PluginMain = fn(callback: HostCallbackProc) -> *mut AEffect;
|
||||
|
||||
/// Host callback function passed to plugin.
|
||||
/// Can be used to query host information from plugin side.
|
||||
pub type HostCallbackProc =
|
||||
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
|
||||
|
||||
/// Dispatcher function used to process opcodes. Called by host.
|
||||
pub type DispatcherProc =
|
||||
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
|
||||
|
||||
/// Process function used to process 32 bit floating point samples. Called by host.
|
||||
pub type ProcessProc =
|
||||
extern "C" fn(effect: *mut AEffect, inputs: *const *const f32, outputs: *mut *mut f32, sample_frames: i32);
|
||||
|
||||
/// Process function used to process 64 bit floating point samples. Called by host.
|
||||
pub type ProcessProcF64 =
|
||||
extern "C" fn(effect: *mut AEffect, inputs: *const *const f64, outputs: *mut *mut f64, sample_frames: i32);
|
||||
|
||||
/// Callback function used to set parameter values. Called by host.
|
||||
pub type SetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32, parameter: f32);
|
||||
|
||||
/// Callback function used to get parameter values. Called by host.
|
||||
pub type GetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32) -> f32;
|
||||
|
||||
/// Used with the VST API to pass around plugin information.
|
||||
#[allow(non_snake_case)]
|
||||
#[repr(C)]
|
||||
pub struct AEffect {
|
||||
/// Magic number. Must be `['V', 'S', 'T', 'P']`.
|
||||
pub magic: i32,
|
||||
|
||||
/// Host to plug-in dispatcher.
|
||||
pub dispatcher: DispatcherProc,
|
||||
|
||||
/// Accumulating process mode is deprecated in VST 2.4! Use `processReplacing` instead!
|
||||
pub _process: ProcessProc,
|
||||
|
||||
/// Set value of automatable parameter.
|
||||
pub setParameter: SetParameterProc,
|
||||
|
||||
/// Get value of automatable parameter.
|
||||
pub getParameter: GetParameterProc,
|
||||
|
||||
/// Number of programs (Presets).
|
||||
pub numPrograms: i32,
|
||||
|
||||
/// Number of parameters. All programs are assumed to have this many parameters.
|
||||
pub numParams: i32,
|
||||
|
||||
/// Number of audio inputs.
|
||||
pub numInputs: i32,
|
||||
|
||||
/// Number of audio outputs.
|
||||
pub numOutputs: i32,
|
||||
|
||||
/// Bitmask made of values from `api::PluginFlags`.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use vst::api::PluginFlags;
|
||||
/// let flags = PluginFlags::CAN_REPLACING | PluginFlags::CAN_DOUBLE_REPLACING;
|
||||
/// // ...
|
||||
/// ```
|
||||
pub flags: i32,
|
||||
|
||||
/// Reserved for host, must be 0.
|
||||
pub reserved1: isize,
|
||||
|
||||
/// Reserved for host, must be 0.
|
||||
pub reserved2: isize,
|
||||
|
||||
/// For algorithms which need input in the first place (Group delay or latency in samples).
|
||||
///
|
||||
/// This value should be initially in a resume state.
|
||||
pub initialDelay: i32,
|
||||
|
||||
/// Deprecated unused member.
|
||||
pub _realQualities: i32,
|
||||
|
||||
/// Deprecated unused member.
|
||||
pub _offQualities: i32,
|
||||
|
||||
/// Deprecated unused member.
|
||||
pub _ioRatio: f32,
|
||||
|
||||
/// Void pointer usable by api to store object data.
|
||||
pub object: *mut c_void,
|
||||
|
||||
/// User defined pointer.
|
||||
pub user: *mut c_void,
|
||||
|
||||
/// Registered unique identifier (register it at Steinberg 3rd party support Web).
|
||||
/// This is used to identify a plug-in during save+load of preset and project.
|
||||
pub uniqueId: i32,
|
||||
|
||||
/// Plug-in version (e.g. 1100 for v1.1.0.0).
|
||||
pub version: i32,
|
||||
|
||||
/// Process audio samples in replacing mode.
|
||||
pub processReplacing: ProcessProc,
|
||||
|
||||
/// Process double-precision audio samples in replacing mode.
|
||||
pub processReplacingF64: ProcessProcF64,
|
||||
|
||||
/// Reserved for future use (please zero).
|
||||
pub future: [u8; 56],
|
||||
}
|
||||
|
||||
impl AEffect {
|
||||
/// Return handle to Plugin object. Only works for plugins created using this library.
|
||||
/// Caller is responsible for not calling this function concurrently.
|
||||
// Suppresses warning about returning a reference to a box
|
||||
#[allow(clippy::borrowed_box)]
|
||||
pub unsafe fn get_plugin(&self) -> &mut Box<dyn Plugin> {
|
||||
//FIXME: find a way to do this without resorting to transmuting via a box
|
||||
&mut *(self.object as *mut Box<dyn Plugin>)
|
||||
}
|
||||
|
||||
/// Return handle to Info object. Only works for plugins created using this library.
|
||||
pub unsafe fn get_info(&self) -> &Info {
|
||||
&(*(self.user as *mut super::PluginCache)).info
|
||||
}
|
||||
|
||||
/// Return handle to PluginParameters object. Only works for plugins created using this library.
|
||||
pub unsafe fn get_params(&self) -> &Arc<dyn PluginParameters> {
|
||||
&(*(self.user as *mut super::PluginCache)).params
|
||||
}
|
||||
|
||||
/// Return handle to Editor object. Only works for plugins created using this library.
|
||||
/// Caller is responsible for not calling this function concurrently.
|
||||
pub unsafe fn get_editor(&self) -> &mut Option<Box<dyn Editor>> {
|
||||
&mut (*(self.user as *mut super::PluginCache)).editor
|
||||
}
|
||||
|
||||
/// Drop the Plugin object. Only works for plugins created using this library.
|
||||
pub unsafe fn drop_plugin(&mut self) {
|
||||
drop(Box::from_raw(self.object as *mut Box<dyn Plugin>));
|
||||
drop(Box::from_raw(self.user as *mut super::PluginCache));
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a channel. Only some hosts use this information.
|
||||
#[repr(C)]
|
||||
pub struct ChannelProperties {
|
||||
/// Channel name.
|
||||
pub name: [u8; MAX_LABEL as usize],
|
||||
|
||||
/// Flags found in `ChannelFlags`.
|
||||
pub flags: i32,
|
||||
|
||||
/// Type of speaker arrangement this channel is a part of.
|
||||
pub arrangement_type: SpeakerArrangementType,
|
||||
|
||||
/// Name of channel (recommended: 6 characters + delimiter).
|
||||
pub short_name: [u8; MAX_SHORT_LABEL as usize],
|
||||
|
||||
/// Reserved for future use.
|
||||
pub future: [u8; 48],
|
||||
}
|
||||
|
||||
/// Tells the host how the channels are intended to be used in the plugin. Only useful for some
|
||||
/// hosts.
|
||||
#[repr(i32)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SpeakerArrangementType {
|
||||
/// User defined arrangement.
|
||||
Custom = -2,
|
||||
/// Empty arrangement.
|
||||
Empty = -1,
|
||||
|
||||
/// Mono.
|
||||
Mono = 0,
|
||||
|
||||
/// L R
|
||||
Stereo,
|
||||
/// Ls Rs
|
||||
StereoSurround,
|
||||
/// Lc Rc
|
||||
StereoCenter,
|
||||
/// Sl Sr
|
||||
StereoSide,
|
||||
/// C Lfe
|
||||
StereoCLfe,
|
||||
|
||||
/// L R C
|
||||
Cinema30,
|
||||
/// L R S
|
||||
Music30,
|
||||
|
||||
/// L R C Lfe
|
||||
Cinema31,
|
||||
/// L R Lfe S
|
||||
Music31,
|
||||
|
||||
/// L R C S (LCRS)
|
||||
Cinema40,
|
||||
/// L R Ls Rs (Quadro)
|
||||
Music40,
|
||||
|
||||
/// L R C Lfe S (LCRS + Lfe)
|
||||
Cinema41,
|
||||
/// L R Lfe Ls Rs (Quadro + Lfe)
|
||||
Music41,
|
||||
|
||||
/// L R C Ls Rs
|
||||
Surround50,
|
||||
/// L R C Lfe Ls Rs
|
||||
Surround51,
|
||||
|
||||
/// L R C Ls Rs Cs
|
||||
Cinema60,
|
||||
/// L R Ls Rs Sl Sr
|
||||
Music60,
|
||||
|
||||
/// L R C Lfe Ls Rs Cs
|
||||
Cinema61,
|
||||
/// L R Lfe Ls Rs Sl Sr
|
||||
Music61,
|
||||
|
||||
/// L R C Ls Rs Lc Rc
|
||||
Cinema70,
|
||||
/// L R C Ls Rs Sl Sr
|
||||
Music70,
|
||||
|
||||
/// L R C Lfe Ls Rs Lc Rc
|
||||
Cinema71,
|
||||
/// L R C Lfe Ls Rs Sl Sr
|
||||
Music71,
|
||||
|
||||
/// L R C Ls Rs Lc Rc Cs
|
||||
Cinema80,
|
||||
/// L R C Ls Rs Cs Sl Sr
|
||||
Music80,
|
||||
|
||||
/// L R C Lfe Ls Rs Lc Rc Cs
|
||||
Cinema81,
|
||||
/// L R C Lfe Ls Rs Cs Sl Sr
|
||||
Music81,
|
||||
|
||||
/// L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
|
||||
Surround102,
|
||||
}
|
||||
|
||||
/// Used to specify whether functionality is supported.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Supported {
|
||||
Yes,
|
||||
Maybe,
|
||||
No,
|
||||
Custom(isize),
|
||||
}
|
||||
|
||||
impl Supported {
|
||||
/// Create a `Supported` value from an integer if possible.
|
||||
pub fn from(val: isize) -> Option<Supported> {
|
||||
use self::Supported::*;
|
||||
|
||||
match val {
|
||||
1 => Some(Yes),
|
||||
0 => Some(Maybe),
|
||||
-1 => Some(No),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<isize> for Supported {
|
||||
/// Convert to integer ordinal for interop with VST api.
|
||||
fn into(self) -> isize {
|
||||
use self::Supported::*;
|
||||
|
||||
match self {
|
||||
Yes => 1,
|
||||
Maybe => 0,
|
||||
No => -1,
|
||||
Custom(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Denotes in which thread the host is in.
|
||||
#[repr(i32)]
|
||||
pub enum ProcessLevel {
|
||||
/// Unsupported by host.
|
||||
Unknown = 0,
|
||||
|
||||
/// GUI thread.
|
||||
User,
|
||||
/// Audio process thread.
|
||||
Realtime,
|
||||
/// Sequence thread (MIDI, etc).
|
||||
Prefetch,
|
||||
/// Offline processing thread (therefore GUI/user thread).
|
||||
Offline,
|
||||
}
|
||||
|
||||
/// Language that the host is using.
|
||||
#[repr(i32)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum HostLanguage {
|
||||
English = 1,
|
||||
German,
|
||||
French,
|
||||
Italian,
|
||||
Spanish,
|
||||
Japanese,
|
||||
}
|
||||
|
||||
/// The file operation to perform.
|
||||
#[repr(i32)]
|
||||
pub enum FileSelectCommand {
|
||||
/// Load a file.
|
||||
Load = 0,
|
||||
/// Save a file.
|
||||
Save,
|
||||
/// Load multiple files simultaneously.
|
||||
LoadMultipleFiles,
|
||||
/// Choose a directory.
|
||||
SelectDirectory,
|
||||
}
|
||||
|
||||
// TODO: investigate removing this.
|
||||
/// Format to select files.
|
||||
pub enum FileSelectType {
|
||||
/// Regular file selector.
|
||||
Regular,
|
||||
}
|
||||
|
||||
/// File type descriptor.
|
||||
#[repr(C)]
|
||||
pub struct FileType {
|
||||
/// Display name of file type.
|
||||
pub name: [u8; 128],
|
||||
|
||||
/// OS X file type.
|
||||
pub osx_type: [u8; 8],
|
||||
/// Windows file type.
|
||||
pub win_type: [u8; 8],
|
||||
/// Unix file type.
|
||||
pub nix_type: [u8; 8],
|
||||
|
||||
/// MIME type.
|
||||
pub mime_type_1: [u8; 128],
|
||||
/// Additional MIME type.
|
||||
pub mime_type_2: [u8; 128],
|
||||
}
|
||||
|
||||
/// File selector descriptor used in `host::OpCode::OpenFileSelector`.
|
||||
#[repr(C)]
|
||||
pub struct FileSelect {
|
||||
/// The type of file selection to perform.
|
||||
pub command: FileSelectCommand,
|
||||
/// The file selector to open.
|
||||
pub select_type: FileSelectType,
|
||||
/// Unknown. 0 = no creator.
|
||||
pub mac_creator: i32,
|
||||
/// Number of file types.
|
||||
pub num_types: i32,
|
||||
/// List of file types to show.
|
||||
pub file_types: *mut FileType,
|
||||
|
||||
/// File selector's title.
|
||||
pub title: [u8; 1024],
|
||||
/// Initial path.
|
||||
pub initial_path: *mut u8,
|
||||
/// Used when operation returns a single path.
|
||||
pub return_path: *mut u8,
|
||||
/// Size of the path buffer in bytes.
|
||||
pub size_return_path: i32,
|
||||
|
||||
/// Used when operation returns multiple paths.
|
||||
pub return_multiple_paths: *mut *mut u8,
|
||||
/// Number of paths returned.
|
||||
pub num_paths: i32,
|
||||
|
||||
/// Reserved by host.
|
||||
pub reserved: isize,
|
||||
/// Reserved for future use.
|
||||
pub future: [u8; 116],
|
||||
}
|
||||
|
||||
/// A struct which contains events.
|
||||
#[repr(C)]
|
||||
pub struct Events {
|
||||
/// Number of events.
|
||||
pub num_events: i32,
|
||||
|
||||
/// Reserved for future use. Should be 0.
|
||||
pub _reserved: isize,
|
||||
|
||||
/// Variable-length array of pointers to `api::Event` objects.
|
||||
///
|
||||
/// The VST standard specifies a variable length array of initial size 2. If there are more
|
||||
/// than 2 elements a larger array must be stored in this structure.
|
||||
pub events: [*mut Event; 2],
|
||||
}
|
||||
|
||||
impl Events {
|
||||
#[inline]
|
||||
pub(crate) fn events_raw(&self) -> &[*const Event] {
|
||||
use std::slice;
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
&self.events[0] as *const *mut _ as *const *const _,
|
||||
self.num_events as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn events_raw_mut(&mut self) -> &mut [*const SysExEvent] {
|
||||
use std::slice;
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
&mut self.events[0] as *mut *mut _ as *mut *const _,
|
||||
self.num_events as usize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this in your impl of process_events() to process the incoming midi events.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use vst::plugin::{Info, Plugin, HostCallback};
|
||||
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
|
||||
/// # use vst::host::Host;
|
||||
/// # use vst::api;
|
||||
/// # use vst::event::{Event, MidiEvent};
|
||||
/// # struct ExamplePlugin { host: HostCallback, send_buf: SendEventBuffer }
|
||||
/// # impl Plugin for ExamplePlugin {
|
||||
/// # fn new(host: HostCallback) -> Self { Self { host, send_buf: Default::default() } }
|
||||
/// #
|
||||
/// # fn get_info(&self) -> Info { Default::default() }
|
||||
/// #
|
||||
/// fn process_events(&mut self, events: &api::Events) {
|
||||
/// for e in events.events() {
|
||||
/// match e {
|
||||
/// Event::Midi(MidiEvent { data, .. }) => {
|
||||
/// // ...
|
||||
/// }
|
||||
/// _ => ()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
pub fn events<'a>(&'a self) -> impl Iterator<Item = crate::event::Event<'a>> {
|
||||
self.events_raw()
|
||||
.iter()
|
||||
.map(|ptr| unsafe { crate::event::Event::from_raw_event(*ptr) })
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of event that has occurred. See `api::Event.event_type`.
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EventType {
|
||||
/// Value used for uninitialized placeholder events.
|
||||
_Placeholder = 0,
|
||||
|
||||
/// Midi event. See `api::MidiEvent`.
|
||||
Midi = 1,
|
||||
|
||||
/// Deprecated.
|
||||
_Audio,
|
||||
/// Deprecated.
|
||||
_Video,
|
||||
/// Deprecated.
|
||||
_Parameter,
|
||||
/// Deprecated.
|
||||
_Trigger,
|
||||
|
||||
/// System exclusive event. See `api::SysExEvent`.
|
||||
SysEx,
|
||||
}
|
||||
|
||||
/// A VST event intended to be casted to a corresponding type.
|
||||
///
|
||||
/// The event types are not all guaranteed to be the same size,
|
||||
/// so casting between them can be done
|
||||
/// via `mem::transmute()` while leveraging pointers, e.g.
|
||||
///
|
||||
/// ```
|
||||
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
|
||||
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
|
||||
/// // let event: *const Event = ...;
|
||||
/// let midi_event: &MidiEvent = unsafe { std::mem::transmute(event) };
|
||||
/// ```
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Event {
|
||||
/// The type of event. This lets you know which event this object should be casted to.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
|
||||
/// #
|
||||
/// # // Valid for test
|
||||
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
|
||||
/// #
|
||||
/// // let mut event: *mut Event = ...
|
||||
/// match unsafe { (*event).event_type } {
|
||||
/// EventType::Midi => {
|
||||
/// let midi_event: &MidiEvent = unsafe {
|
||||
/// std::mem::transmute(event)
|
||||
/// };
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
/// EventType::SysEx => {
|
||||
/// let sys_event: &SysExEvent = unsafe {
|
||||
/// std::mem::transmute(event)
|
||||
/// };
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
/// // ...
|
||||
/// # _ => {}
|
||||
/// }
|
||||
/// ```
|
||||
pub event_type: EventType,
|
||||
|
||||
/// Size of this structure; `mem::sizeof::<Event>()`.
|
||||
pub byte_size: i32,
|
||||
|
||||
/// Number of samples into the current processing block that this event occurs on.
|
||||
///
|
||||
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||
/// `samples[123]`.
|
||||
pub delta_frames: i32,
|
||||
|
||||
/// Generic flags, none defined in VST api yet.
|
||||
pub _flags: i32,
|
||||
|
||||
/// The `Event` type is cast appropriately, so this acts as reserved space.
|
||||
///
|
||||
/// The actual size of the data may vary
|
||||
///as this type is not guaranteed to be the same size as the other event types.
|
||||
pub _reserved: [u8; 16],
|
||||
}
|
||||
|
||||
/// A midi event.
|
||||
#[repr(C)]
|
||||
pub struct MidiEvent {
|
||||
/// Should be `EventType::Midi`.
|
||||
pub event_type: EventType,
|
||||
|
||||
/// Size of this structure; `mem::sizeof::<MidiEvent>()`.
|
||||
pub byte_size: i32,
|
||||
|
||||
/// Number of samples into the current processing block that this event occurs on.
|
||||
///
|
||||
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||
/// `samples[123]`.
|
||||
pub delta_frames: i32,
|
||||
|
||||
/// See `MidiEventFlags`.
|
||||
pub flags: i32,
|
||||
|
||||
/// Length in sample frames of entire note if available, otherwise 0.
|
||||
pub note_length: i32,
|
||||
|
||||
/// Offset in samples into note from start if available, otherwise 0.
|
||||
pub note_offset: i32,
|
||||
|
||||
/// 1 to 3 midi bytes. TODO: Doc
|
||||
pub midi_data: [u8; 3],
|
||||
|
||||
/// Reserved midi byte (0).
|
||||
pub _midi_reserved: u8,
|
||||
|
||||
/// Detuning between -63 and +64 cents,
|
||||
/// for scales other than 'well-tempered'. e.g. 'microtuning'
|
||||
pub detune: i8,
|
||||
|
||||
/// Note off velocity between 0 and 127.
|
||||
pub note_off_velocity: u8,
|
||||
|
||||
/// Reserved for future use. Should be 0.
|
||||
pub _reserved1: u8,
|
||||
/// Reserved for future use. Should be 0.
|
||||
pub _reserved2: u8,
|
||||
}
|
||||
|
||||
/// A midi system exclusive event.
|
||||
///
|
||||
/// This event only contains raw byte data, and is up to the plugin to interpret it correctly.
|
||||
/// `plugin::CanDo` has a `ReceiveSysExEvent` variant which lets the host query the plugin as to
|
||||
/// whether this event is supported.
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
pub struct SysExEvent {
|
||||
/// Should be `EventType::SysEx`.
|
||||
pub event_type: EventType,
|
||||
|
||||
/// Size of this structure; `mem::sizeof::<SysExEvent>()`.
|
||||
pub byte_size: i32,
|
||||
|
||||
/// Number of samples into the current processing block that this event occurs on.
|
||||
///
|
||||
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||
/// `samples[123]`.
|
||||
pub delta_frames: i32,
|
||||
|
||||
/// Generic flags, none defined in VST api yet.
|
||||
pub _flags: i32,
|
||||
|
||||
/// Size of payload in bytes.
|
||||
pub data_size: i32,
|
||||
|
||||
/// Reserved for future use. Should be 0.
|
||||
pub _reserved1: isize,
|
||||
|
||||
/// Pointer to payload.
|
||||
pub system_data: *mut u8,
|
||||
|
||||
/// Reserved for future use. Should be 0.
|
||||
pub _reserved2: isize,
|
||||
}
|
||||
|
||||
unsafe impl Send for SysExEvent {}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Default, Copy)]
|
||||
/// Describes the time at the start of the block currently being processed
|
||||
pub struct TimeInfo {
|
||||
/// current Position in audio samples (always valid)
|
||||
pub sample_pos: f64,
|
||||
|
||||
/// current Sample Rate in Hertz (always valid)
|
||||
pub sample_rate: f64,
|
||||
|
||||
/// System Time in nanoseconds (10^-9 second)
|
||||
pub nanoseconds: f64,
|
||||
|
||||
/// Musical Position, in Quarter Note (1.0 equals 1 Quarter Note)
|
||||
pub ppq_pos: f64,
|
||||
|
||||
/// current Tempo in BPM (Beats Per Minute)
|
||||
pub tempo: f64,
|
||||
|
||||
/// last Bar Start Position, in Quarter Note
|
||||
pub bar_start_pos: f64,
|
||||
|
||||
/// Cycle Start (left locator), in Quarter Note
|
||||
pub cycle_start_pos: f64,
|
||||
|
||||
/// Cycle End (right locator), in Quarter Note
|
||||
pub cycle_end_pos: f64,
|
||||
|
||||
/// Time Signature Numerator (e.g. 3 for 3/4)
|
||||
pub time_sig_numerator: i32,
|
||||
|
||||
/// Time Signature Denominator (e.g. 4 for 3/4)
|
||||
pub time_sig_denominator: i32,
|
||||
|
||||
/// SMPTE offset in SMPTE subframes (bits; 1/80 of a frame).
|
||||
/// The current SMPTE position can be calculated using `sample_pos`, `sample_rate`, and `smpte_frame_rate`.
|
||||
pub smpte_offset: i32,
|
||||
|
||||
/// See `SmpteFrameRate`
|
||||
pub smpte_frame_rate: SmpteFrameRate,
|
||||
|
||||
/// MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock)
|
||||
pub samples_to_next_clock: i32,
|
||||
|
||||
/// See `TimeInfoFlags`
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
/// SMPTE Frame Rates.
|
||||
pub enum SmpteFrameRate {
|
||||
/// 24 fps
|
||||
Smpte24fps = 0,
|
||||
/// 25 fps
|
||||
Smpte25fps = 1,
|
||||
/// 29.97 fps
|
||||
Smpte2997fps = 2,
|
||||
/// 30 fps
|
||||
Smpte30fps = 3,
|
||||
|
||||
/// 29.97 drop
|
||||
Smpte2997dfps = 4,
|
||||
/// 30 drop
|
||||
Smpte30dfps = 5,
|
||||
|
||||
/// Film 16mm
|
||||
SmpteFilm16mm = 6,
|
||||
/// Film 35mm
|
||||
SmpteFilm35mm = 7,
|
||||
|
||||
/// HDTV: 23.976 fps
|
||||
Smpte239fps = 10,
|
||||
/// HDTV: 24.976 fps
|
||||
Smpte249fps = 11,
|
||||
/// HDTV: 59.94 fps
|
||||
Smpte599fps = 12,
|
||||
/// HDTV: 60 fps
|
||||
Smpte60fps = 13,
|
||||
}
|
||||
impl Default for SmpteFrameRate {
|
||||
fn default() -> Self {
|
||||
SmpteFrameRate::Smpte24fps
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Flags for VST channels.
|
||||
pub struct ChannelFlags: i32 {
|
||||
/// Indicates channel is active. Ignored by host.
|
||||
const ACTIVE = 1;
|
||||
/// Indicates channel is first of stereo pair.
|
||||
const STEREO = 1 << 1;
|
||||
/// Use channel's specified speaker_arrangement instead of stereo flag.
|
||||
const SPEAKER = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Flags for VST plugins.
|
||||
pub struct PluginFlags: i32 {
|
||||
/// Plugin has an editor.
|
||||
const HAS_EDITOR = 1;
|
||||
/// Plugin can process 32 bit audio. (Mandatory in VST 2.4).
|
||||
const CAN_REPLACING = 1 << 4;
|
||||
/// Plugin preset data is handled in formatless chunks.
|
||||
const PROGRAM_CHUNKS = 1 << 5;
|
||||
/// Plugin is a synth.
|
||||
const IS_SYNTH = 1 << 8;
|
||||
/// Plugin does not produce sound when all input is silence.
|
||||
const NO_SOUND_IN_STOP = 1 << 9;
|
||||
/// Supports 64 bit audio processing.
|
||||
const CAN_DOUBLE_REPLACING = 1 << 12;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Cross platform modifier key flags.
|
||||
pub struct ModifierKey: u8 {
|
||||
/// Shift key.
|
||||
const SHIFT = 1;
|
||||
/// Alt key.
|
||||
const ALT = 1 << 1;
|
||||
/// Control on mac.
|
||||
const COMMAND = 1 << 2;
|
||||
/// Command on mac, ctrl on other.
|
||||
const CONTROL = 1 << 3; // Ctrl on PC, Apple on Mac
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// MIDI event flags.
|
||||
pub struct MidiEventFlags: i32 {
|
||||
/// This event is played live (not in playback from a sequencer track). This allows the
|
||||
/// plugin to handle these flagged events with higher priority, especially when the
|
||||
/// plugin has a big latency as per `plugin::Info::initial_delay`.
|
||||
const REALTIME_EVENT = 1;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Used in the `flags` field of `TimeInfo`, and for querying the host for specific values
|
||||
pub struct TimeInfoFlags : i32 {
|
||||
/// Indicates that play, cycle or record state has changed.
|
||||
const TRANSPORT_CHANGED = 1;
|
||||
/// Set if Host sequencer is currently playing.
|
||||
const TRANSPORT_PLAYING = 1 << 1;
|
||||
/// Set if Host sequencer is in cycle mode.
|
||||
const TRANSPORT_CYCLE_ACTIVE = 1 << 2;
|
||||
/// Set if Host sequencer is in record mode.
|
||||
const TRANSPORT_RECORDING = 1 << 3;
|
||||
|
||||
/// Set if automation write mode active (record parameter changes).
|
||||
const AUTOMATION_WRITING = 1 << 6;
|
||||
/// Set if automation read mode active (play parameter changes).
|
||||
const AUTOMATION_READING = 1 << 7;
|
||||
|
||||
/// Set if TimeInfo::nanoseconds is valid.
|
||||
const NANOSECONDS_VALID = 1 << 8;
|
||||
/// Set if TimeInfo::ppq_pos is valid.
|
||||
const PPQ_POS_VALID = 1 << 9;
|
||||
/// Set if TimeInfo::tempo is valid.
|
||||
const TEMPO_VALID = 1 << 10;
|
||||
/// Set if TimeInfo::bar_start_pos is valid.
|
||||
const BARS_VALID = 1 << 11;
|
||||
/// Set if both TimeInfo::cycle_start_pos and VstTimeInfo::cycle_end_pos are valid.
|
||||
const CYCLE_POS_VALID = 1 << 12;
|
||||
/// Set if both TimeInfo::time_sig_numerator and TimeInfo::time_sig_denominator are valid.
|
||||
const TIME_SIG_VALID = 1 << 13;
|
||||
/// Set if both TimeInfo::smpte_offset and VstTimeInfo::smpte_frame_rate are valid.
|
||||
const SMPTE_VALID = 1 << 14;
|
||||
/// Set if TimeInfo::samples_to_next_clock is valid.
|
||||
const VST_CLOCK_VALID = 1 << 15;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::event;
|
||||
use super::*;
|
||||
use std::mem;
|
||||
|
||||
// This container is used because we have to store somewhere the events
|
||||
// that are pointed to by raw pointers in the events object. We heap allocate
|
||||
// the event so the pointer in events stays consistent when the container is moved.
|
||||
pub struct EventContainer {
|
||||
stored_event: Box<Event>,
|
||||
pub events: Events,
|
||||
}
|
||||
|
||||
// A convenience method which creates an api::Events object representing a midi event.
|
||||
// This represents code that might be found in a VST host using this API.
|
||||
fn encode_midi_message_as_events(message: [u8; 3]) -> EventContainer {
|
||||
let midi_event: MidiEvent = MidiEvent {
|
||||
event_type: EventType::Midi,
|
||||
byte_size: mem::size_of::<MidiEvent>() as i32,
|
||||
delta_frames: 0,
|
||||
flags: 0,
|
||||
note_length: 0,
|
||||
note_offset: 0,
|
||||
midi_data: [message[0], message[1], message[2]],
|
||||
_midi_reserved: 0,
|
||||
detune: 0,
|
||||
note_off_velocity: 0,
|
||||
_reserved1: 0,
|
||||
_reserved2: 0,
|
||||
};
|
||||
let mut event: Event = unsafe { std::mem::transmute(midi_event) };
|
||||
event.event_type = EventType::Midi;
|
||||
|
||||
let events = Events {
|
||||
num_events: 1,
|
||||
_reserved: 0,
|
||||
events: [&mut event, &mut event], // Second one is a dummy
|
||||
};
|
||||
let mut ec = EventContainer {
|
||||
stored_event: Box::new(event),
|
||||
events,
|
||||
};
|
||||
ec.events.events[0] = &mut *(ec.stored_event); // Overwrite ptrs, since we moved the event into ec
|
||||
ec
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_and_decode_gives_back_original_message() {
|
||||
let message: [u8; 3] = [35, 16, 22];
|
||||
let encoded = encode_midi_message_as_events(message);
|
||||
assert_eq!(encoded.events.num_events, 1);
|
||||
assert_eq!(encoded.events.events.len(), 2);
|
||||
let e_vec: Vec<event::Event> = encoded.events.events().collect();
|
||||
assert_eq!(e_vec.len(), 1);
|
||||
|
||||
match e_vec[0] {
|
||||
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||
assert_eq!(data, message);
|
||||
}
|
||||
_ => {
|
||||
panic!("Not a midi event!");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// This is a regression test for a bug fixed in PR #93
|
||||
// We check here that calling events() on an api::Events object
|
||||
// does not mutate the underlying events.
|
||||
#[test]
|
||||
fn message_survives_calling_events() {
|
||||
let message: [u8; 3] = [35, 16, 22];
|
||||
let encoded = encode_midi_message_as_events(message);
|
||||
|
||||
for e in encoded.events.events() {
|
||||
match e {
|
||||
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||
assert_eq!(data, message);
|
||||
}
|
||||
_ => {
|
||||
panic!("Not a midi event!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for e in encoded.events.events() {
|
||||
match e {
|
||||
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||
assert_eq!(data, message);
|
||||
}
|
||||
_ => {
|
||||
panic!("Not a midi event!"); // FAILS here!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
606
plugin/vst/src/buffer.rs
Normal file
606
plugin/vst/src/buffer.rs
Normal file
|
@ -0,0 +1,606 @@
|
|||
//! Buffers to safely work with audio samples.
|
||||
|
||||
use num_traits::Float;
|
||||
|
||||
use std::slice;
|
||||
|
||||
/// `AudioBuffer` contains references to the audio buffers for all input and output channels.
|
||||
///
|
||||
/// To create an `AudioBuffer` in a host, use a [`HostBuffer`](../host/struct.HostBuffer.html).
|
||||
pub struct AudioBuffer<'a, T: 'a + Float> {
|
||||
inputs: &'a [*const T],
|
||||
outputs: &'a mut [*mut T],
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a + Float> AudioBuffer<'a, T> {
|
||||
/// Create an `AudioBuffer` from raw pointers.
|
||||
/// Only really useful for interacting with the VST API.
|
||||
#[inline]
|
||||
pub unsafe fn from_raw(
|
||||
input_count: usize,
|
||||
output_count: usize,
|
||||
inputs_raw: *const *const T,
|
||||
outputs_raw: *mut *mut T,
|
||||
samples: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
inputs: slice::from_raw_parts(inputs_raw, input_count),
|
||||
outputs: slice::from_raw_parts_mut(outputs_raw, output_count),
|
||||
samples,
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of input channels that this buffer was created for
|
||||
#[inline]
|
||||
pub fn input_count(&self) -> usize {
|
||||
self.inputs.len()
|
||||
}
|
||||
|
||||
/// The number of output channels that this buffer was created for
|
||||
#[inline]
|
||||
pub fn output_count(&self) -> usize {
|
||||
self.outputs.len()
|
||||
}
|
||||
|
||||
/// The number of samples in this buffer (same for all channels)
|
||||
#[inline]
|
||||
pub fn samples(&self) -> usize {
|
||||
self.samples
|
||||
}
|
||||
|
||||
/// The raw inputs to pass to processReplacing
|
||||
#[inline]
|
||||
pub(crate) fn raw_inputs(&self) -> &[*const T] {
|
||||
self.inputs
|
||||
}
|
||||
|
||||
/// The raw outputs to pass to processReplacing
|
||||
#[inline]
|
||||
pub(crate) fn raw_outputs(&mut self) -> &mut [*mut T] {
|
||||
&mut self.outputs
|
||||
}
|
||||
|
||||
/// Split this buffer into separate inputs and outputs.
|
||||
#[inline]
|
||||
pub fn split<'b>(&'b mut self) -> (Inputs<'b, T>, Outputs<'b, T>)
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
(
|
||||
Inputs {
|
||||
bufs: self.inputs,
|
||||
samples: self.samples,
|
||||
},
|
||||
Outputs {
|
||||
bufs: self.outputs,
|
||||
samples: self.samples,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Create an iterator over pairs of input buffers and output buffers.
|
||||
#[inline]
|
||||
pub fn zip<'b>(&'b mut self) -> AudioBufferIterator<'a, 'b, T> {
|
||||
AudioBufferIterator {
|
||||
audio_buffer: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over pairs of buffers of input channels and output channels.
|
||||
pub struct AudioBufferIterator<'a, 'b, T>
|
||||
where
|
||||
T: 'a + Float,
|
||||
'a: 'b,
|
||||
{
|
||||
audio_buffer: &'b mut AudioBuffer<'a, T>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T> Iterator for AudioBufferIterator<'a, 'b, T>
|
||||
where
|
||||
T: 'b + Float,
|
||||
{
|
||||
type Item = (&'b [T], &'b mut [T]);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index < self.audio_buffer.inputs.len() && self.index < self.audio_buffer.outputs.len() {
|
||||
let input =
|
||||
unsafe { slice::from_raw_parts(self.audio_buffer.inputs[self.index], self.audio_buffer.samples) };
|
||||
let output =
|
||||
unsafe { slice::from_raw_parts_mut(self.audio_buffer.outputs[self.index], self.audio_buffer.samples) };
|
||||
let val = (input, output);
|
||||
self.index += 1;
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
/// Wrapper type to access the buffers for the input channels of an `AudioBuffer` in a safe way.
|
||||
/// Behaves like a slice.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Inputs<'a, T: 'a> {
|
||||
bufs: &'a [*const T],
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> Inputs<'a, T> {
|
||||
/// Number of channels
|
||||
pub fn len(&self) -> usize {
|
||||
self.bufs.len()
|
||||
}
|
||||
|
||||
/// Returns true if the buffer is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Access channel at the given index
|
||||
pub fn get(&self, i: usize) -> &'a [T] {
|
||||
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
|
||||
}
|
||||
|
||||
/// Split borrowing at the given index, like for slices
|
||||
pub fn split_at(&self, i: usize) -> (Inputs<'a, T>, Inputs<'a, T>) {
|
||||
let (l, r) = self.bufs.split_at(i);
|
||||
(
|
||||
Inputs {
|
||||
bufs: l,
|
||||
samples: self.samples,
|
||||
},
|
||||
Inputs {
|
||||
bufs: r,
|
||||
samples: self.samples,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Index<usize> for Inputs<'a, T> {
|
||||
type Output = [T];
|
||||
|
||||
fn index(&self, i: usize) -> &Self::Output {
|
||||
self.get(i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over buffers for input channels of an `AudioBuffer`.
|
||||
pub struct InputIterator<'a, T: 'a> {
|
||||
data: Inputs<'a, T>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for InputIterator<'a, T> {
|
||||
type Item = &'a [T];
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.i < self.data.len() {
|
||||
let val = self.data.get(self.i);
|
||||
self.i += 1;
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Sized> IntoIterator for Inputs<'a, T> {
|
||||
type Item = &'a [T];
|
||||
type IntoIter = InputIterator<'a, T>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
InputIterator { data: self, i: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type to access the buffers for the output channels of an `AudioBuffer` in a safe way.
|
||||
/// Behaves like a slice.
|
||||
pub struct Outputs<'a, T: 'a> {
|
||||
bufs: &'a [*mut T],
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> Outputs<'a, T> {
|
||||
/// Number of channels
|
||||
pub fn len(&self) -> usize {
|
||||
self.bufs.len()
|
||||
}
|
||||
|
||||
/// Returns true if the buffer is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Access channel at the given index
|
||||
pub fn get(&self, i: usize) -> &'a [T] {
|
||||
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
|
||||
}
|
||||
|
||||
/// Mutably access channel at the given index
|
||||
pub fn get_mut(&mut self, i: usize) -> &'a mut [T] {
|
||||
unsafe { slice::from_raw_parts_mut(self.bufs[i], self.samples) }
|
||||
}
|
||||
|
||||
/// Split borrowing at the given index, like for slices
|
||||
pub fn split_at_mut(self, i: usize) -> (Outputs<'a, T>, Outputs<'a, T>) {
|
||||
let (l, r) = self.bufs.split_at(i);
|
||||
(
|
||||
Outputs {
|
||||
bufs: l,
|
||||
samples: self.samples,
|
||||
},
|
||||
Outputs {
|
||||
bufs: r,
|
||||
samples: self.samples,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Index<usize> for Outputs<'a, T> {
|
||||
type Output = [T];
|
||||
|
||||
fn index(&self, i: usize) -> &Self::Output {
|
||||
self.get(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IndexMut<usize> for Outputs<'a, T> {
|
||||
fn index_mut(&mut self, i: usize) -> &mut Self::Output {
|
||||
self.get_mut(i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over buffers for output channels of an `AudioBuffer`.
|
||||
pub struct OutputIterator<'a, 'b, T>
|
||||
where
|
||||
T: 'a,
|
||||
'a: 'b,
|
||||
{
|
||||
data: &'b mut Outputs<'a, T>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T> Iterator for OutputIterator<'a, 'b, T>
|
||||
where
|
||||
T: 'b,
|
||||
{
|
||||
type Item = &'b mut [T];
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.i < self.data.len() {
|
||||
let val = self.data.get_mut(self.i);
|
||||
self.i += 1;
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T: Sized> IntoIterator for &'b mut Outputs<'a, T> {
|
||||
type Item = &'b mut [T];
|
||||
type IntoIter = OutputIterator<'a, 'b, T>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
OutputIterator { data: self, i: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
use crate::event::{Event, MidiEvent, SysExEvent};
|
||||
|
||||
/// This is used as a placeholder to pre-allocate space for a fixed number of
|
||||
/// midi events in the re-useable `SendEventBuffer`, because `SysExEvent` is
|
||||
/// larger than `MidiEvent`, so either one can be stored in a `SysExEvent`.
|
||||
pub type PlaceholderEvent = api::SysExEvent;
|
||||
|
||||
/// This trait is used by `SendEventBuffer::send_events` to accept iterators over midi events
|
||||
pub trait WriteIntoPlaceholder {
|
||||
/// writes an event into the given placeholder memory location
|
||||
fn write_into(&self, out: &mut PlaceholderEvent);
|
||||
}
|
||||
|
||||
impl<'a, T: WriteIntoPlaceholder> WriteIntoPlaceholder for &'a T {
|
||||
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||
(*self).write_into(out);
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteIntoPlaceholder for MidiEvent {
|
||||
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||
let out = unsafe { &mut *(out as *mut _ as *mut _) };
|
||||
*out = api::MidiEvent {
|
||||
event_type: api::EventType::Midi,
|
||||
byte_size: mem::size_of::<api::MidiEvent>() as i32,
|
||||
delta_frames: self.delta_frames,
|
||||
flags: if self.live {
|
||||
api::MidiEventFlags::REALTIME_EVENT.bits()
|
||||
} else {
|
||||
0
|
||||
},
|
||||
note_length: self.note_length.unwrap_or(0),
|
||||
note_offset: self.note_offset.unwrap_or(0),
|
||||
midi_data: self.data,
|
||||
_midi_reserved: 0,
|
||||
detune: self.detune,
|
||||
note_off_velocity: self.note_off_velocity,
|
||||
_reserved1: 0,
|
||||
_reserved2: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteIntoPlaceholder for SysExEvent<'a> {
|
||||
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||
*out = PlaceholderEvent {
|
||||
event_type: api::EventType::SysEx,
|
||||
byte_size: mem::size_of::<PlaceholderEvent>() as i32,
|
||||
delta_frames: self.delta_frames,
|
||||
_flags: 0,
|
||||
data_size: self.payload.len() as i32,
|
||||
_reserved1: 0,
|
||||
system_data: self.payload.as_ptr() as *const u8 as *mut u8,
|
||||
_reserved2: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WriteIntoPlaceholder for Event<'a> {
|
||||
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||
match *self {
|
||||
Event::Midi(ref ev) => {
|
||||
ev.write_into(out);
|
||||
}
|
||||
Event::SysEx(ref ev) => {
|
||||
ev.write_into(out);
|
||||
}
|
||||
Event::Deprecated(e) => {
|
||||
let out = unsafe { &mut *(out as *mut _ as *mut _) };
|
||||
*out = e;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{api, host::Host};
|
||||
use std::mem;
|
||||
|
||||
/// This buffer is used for sending midi events through the VST interface.
|
||||
/// The purpose of this is to convert outgoing midi events from `event::Event` to `api::Events`.
|
||||
/// It only allocates memory in new() and reuses the memory between calls.
|
||||
pub struct SendEventBuffer {
|
||||
buf: Vec<u8>,
|
||||
api_events: Vec<PlaceholderEvent>, // using SysExEvent to store both because it's larger than MidiEvent
|
||||
}
|
||||
|
||||
impl Default for SendEventBuffer {
|
||||
fn default() -> Self {
|
||||
SendEventBuffer::new(1024)
|
||||
}
|
||||
}
|
||||
|
||||
impl SendEventBuffer {
|
||||
/// Creates a buffer for sending up to the given number of midi events per frame
|
||||
#[inline(always)]
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
let header_size = mem::size_of::<api::Events>() - (mem::size_of::<*mut api::Event>() * 2);
|
||||
let body_size = mem::size_of::<*mut api::Event>() * capacity;
|
||||
let mut buf = vec![0u8; header_size + body_size];
|
||||
let api_events = vec![unsafe { mem::zeroed::<PlaceholderEvent>() }; capacity];
|
||||
{
|
||||
let ptrs = {
|
||||
let e = Self::buf_as_api_events(&mut buf);
|
||||
e.num_events = capacity as i32;
|
||||
e.events_raw_mut()
|
||||
};
|
||||
for (ptr, event) in ptrs.iter_mut().zip(&api_events) {
|
||||
let (ptr, event): (&mut *const PlaceholderEvent, &PlaceholderEvent) = (ptr, event);
|
||||
*ptr = event;
|
||||
}
|
||||
}
|
||||
Self { buf, api_events }
|
||||
}
|
||||
|
||||
/// Sends events to the host. See the `fwd_midi` example.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use vst::plugin::{Info, Plugin, HostCallback};
|
||||
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
|
||||
/// # use vst::host::Host;
|
||||
/// # use vst::event::*;
|
||||
/// # struct ExamplePlugin { host: HostCallback, send_buffer: SendEventBuffer }
|
||||
/// # impl Plugin for ExamplePlugin {
|
||||
/// # fn new(host: HostCallback) -> Self { Self { host, send_buffer: Default::default() } }
|
||||
/// #
|
||||
/// # fn get_info(&self) -> Info { Default::default() }
|
||||
/// #
|
||||
/// fn process(&mut self, buffer: &mut AudioBuffer<f32>){
|
||||
/// let events: Vec<MidiEvent> = vec![
|
||||
/// // ...
|
||||
/// ];
|
||||
/// self.send_buffer.send_events(&events, &mut self.host);
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn send_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T, host: &mut dyn Host) {
|
||||
self.store_events(events);
|
||||
host.process_events(self.events());
|
||||
}
|
||||
|
||||
/// Stores events in the buffer, replacing the buffer's current content.
|
||||
/// Use this in [`process_events`](crate::Plugin::process_events) to store received input events, then read them in [`process`](crate::Plugin::process) using [`events`](SendEventBuffer::events).
|
||||
#[inline(always)]
|
||||
pub fn store_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T) {
|
||||
#[allow(clippy::suspicious_map)]
|
||||
let count = events
|
||||
.into_iter()
|
||||
.zip(self.api_events.iter_mut())
|
||||
.map(|(ev, out)| ev.write_into(out))
|
||||
.count();
|
||||
self.set_num_events(count);
|
||||
}
|
||||
|
||||
/// Returns a reference to the stored events
|
||||
#[inline(always)]
|
||||
pub fn events(&self) -> &api::Events {
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
unsafe {
|
||||
&*(self.buf.as_ptr() as *const api::Events)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the buffer
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.set_num_events(0);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn buf_as_api_events(buf: &mut [u8]) -> &mut api::Events {
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
unsafe {
|
||||
&mut *(buf.as_mut_ptr() as *mut api::Events)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn set_num_events(&mut self, events_len: usize) {
|
||||
use std::cmp::min;
|
||||
let e = Self::buf_as_api_events(&mut self.buf);
|
||||
e.num_events = min(self.api_events.len(), events_len) as i32;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::AudioBuffer;
|
||||
|
||||
/// Size of buffers used in tests.
|
||||
const SIZE: usize = 1024;
|
||||
|
||||
/// Test that creating and zipping buffers works.
|
||||
///
|
||||
/// This test creates a channel for 2 inputs and 2 outputs.
|
||||
/// The input channels are simply values
|
||||
/// from 0 to `SIZE-1` (e.g. [0, 1, 2, 3, 4, .. , SIZE - 1])
|
||||
/// and the output channels are just 0.
|
||||
/// This test assures that when the buffers are zipped together,
|
||||
/// the input values do not change.
|
||||
#[test]
|
||||
fn buffer_zip() {
|
||||
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
|
||||
let in2 = in1.clone();
|
||||
|
||||
let mut out1 = vec![0.0; SIZE];
|
||||
let mut out2 = out1.clone();
|
||||
|
||||
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||
|
||||
for (input, output) in buffer.zip() {
|
||||
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
|
||||
assert_eq!(*input, acc as f32);
|
||||
assert_eq!(*output, 0.0);
|
||||
acc + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the `zip()` method returns an iterator that gives `n` elements
|
||||
// where n is the number of inputs when this is lower than the number of outputs.
|
||||
#[test]
|
||||
fn buffer_zip_fewer_inputs_than_outputs() {
|
||||
let in1 = vec![1.0; SIZE];
|
||||
let in2 = vec![2.0; SIZE];
|
||||
|
||||
let mut out1 = vec![3.0; SIZE];
|
||||
let mut out2 = vec![4.0; SIZE];
|
||||
let mut out3 = vec![5.0; SIZE];
|
||||
|
||||
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr(), out3.as_mut_ptr()];
|
||||
let mut buffer = unsafe { AudioBuffer::from_raw(2, 3, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||
|
||||
let mut iter = buffer.zip();
|
||||
if let Some((observed_in1, observed_out1)) = iter.next() {
|
||||
assert_eq!(1.0, observed_in1[0]);
|
||||
assert_eq!(3.0, observed_out1[0]);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
if let Some((observed_in2, observed_out2)) = iter.next() {
|
||||
assert_eq!(2.0, observed_in2[0]);
|
||||
assert_eq!(4.0, observed_out2[0]);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
assert_eq!(None, iter.next());
|
||||
}
|
||||
|
||||
// Test that the `zip()` method returns an iterator that gives `n` elements
|
||||
// where n is the number of outputs when this is lower than the number of inputs.
|
||||
#[test]
|
||||
fn buffer_zip_more_inputs_than_outputs() {
|
||||
let in1 = vec![1.0; SIZE];
|
||||
let in2 = vec![2.0; SIZE];
|
||||
let in3 = vec![3.0; SIZE];
|
||||
|
||||
let mut out1 = vec![4.0; SIZE];
|
||||
let mut out2 = vec![5.0; SIZE];
|
||||
|
||||
let inputs = vec![in1.as_ptr(), in2.as_ptr(), in3.as_ptr()];
|
||||
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||
let mut buffer = unsafe { AudioBuffer::from_raw(3, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||
|
||||
let mut iter = buffer.zip();
|
||||
|
||||
if let Some((observed_in1, observed_out1)) = iter.next() {
|
||||
assert_eq!(1.0, observed_in1[0]);
|
||||
assert_eq!(4.0, observed_out1[0]);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
if let Some((observed_in2, observed_out2)) = iter.next() {
|
||||
assert_eq!(2.0, observed_in2[0]);
|
||||
assert_eq!(5.0, observed_out2[0]);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
assert_eq!(None, iter.next());
|
||||
}
|
||||
|
||||
/// Test that creating buffers from raw pointers works.
|
||||
#[test]
|
||||
fn from_raw() {
|
||||
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
|
||||
let in2 = in1.clone();
|
||||
|
||||
let mut out1 = vec![0.0; SIZE];
|
||||
let mut out2 = out1.clone();
|
||||
|
||||
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||
|
||||
for (input, output) in buffer.zip() {
|
||||
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
|
||||
assert_eq!(*input, acc as f32);
|
||||
assert_eq!(*output, 0.0);
|
||||
acc + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
19
plugin/vst/src/cache.rs
Normal file
19
plugin/vst/src/cache.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{editor::Editor, prelude::*};
|
||||
|
||||
pub(crate) struct PluginCache {
|
||||
pub info: Info,
|
||||
pub params: Arc<dyn PluginParameters>,
|
||||
pub editor: Option<Box<dyn Editor>>,
|
||||
}
|
||||
|
||||
impl PluginCache {
|
||||
pub fn new(info: &Info, params: Arc<dyn PluginParameters>, editor: Option<Box<dyn Editor>>) -> Self {
|
||||
Self {
|
||||
info: info.clone(),
|
||||
params,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
}
|
352
plugin/vst/src/channels.rs
Normal file
352
plugin/vst/src/channels.rs
Normal file
|
@ -0,0 +1,352 @@
|
|||
//! Meta data for dealing with input / output channels. Not all hosts use this so it is not
|
||||
//! necessary for plugin functionality.
|
||||
|
||||
use crate::api;
|
||||
use crate::api::consts::{MAX_LABEL, MAX_SHORT_LABEL};
|
||||
|
||||
/// Information about an input / output channel. This isn't necessary for a channel to function but
|
||||
/// informs the host how the channel is meant to be used.
|
||||
pub struct ChannelInfo {
|
||||
name: String,
|
||||
short_name: String,
|
||||
active: bool,
|
||||
arrangement_type: SpeakerArrangementType,
|
||||
}
|
||||
|
||||
impl ChannelInfo {
|
||||
/// Construct a new `ChannelInfo` object.
|
||||
///
|
||||
/// `name` is a user friendly name for this channel limited to `MAX_LABEL` characters.
|
||||
/// `short_name` is an optional field which provides a short name limited to `MAX_SHORT_LABEL`.
|
||||
/// `active` determines whether this channel is active.
|
||||
/// `arrangement_type` describes the arrangement type for this channel.
|
||||
pub fn new(
|
||||
name: String,
|
||||
short_name: Option<String>,
|
||||
active: bool,
|
||||
arrangement_type: Option<SpeakerArrangementType>,
|
||||
) -> ChannelInfo {
|
||||
ChannelInfo {
|
||||
name: name.clone(),
|
||||
|
||||
short_name: if let Some(short_name) = short_name {
|
||||
short_name
|
||||
} else {
|
||||
name
|
||||
},
|
||||
|
||||
active,
|
||||
|
||||
arrangement_type: arrangement_type.unwrap_or(SpeakerArrangementType::Custom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<api::ChannelProperties> for ChannelInfo {
|
||||
/// Convert to the VST api equivalent of this structure.
|
||||
fn into(self) -> api::ChannelProperties {
|
||||
api::ChannelProperties {
|
||||
name: {
|
||||
let mut label = [0; MAX_LABEL as usize];
|
||||
for (b, c) in self.name.bytes().zip(label.iter_mut()) {
|
||||
*c = b;
|
||||
}
|
||||
label
|
||||
},
|
||||
flags: {
|
||||
let mut flag = api::ChannelFlags::empty();
|
||||
if self.active {
|
||||
flag |= api::ChannelFlags::ACTIVE
|
||||
}
|
||||
if self.arrangement_type.is_left_stereo() {
|
||||
flag |= api::ChannelFlags::STEREO
|
||||
}
|
||||
if self.arrangement_type.is_speaker_type() {
|
||||
flag |= api::ChannelFlags::SPEAKER
|
||||
}
|
||||
flag.bits()
|
||||
},
|
||||
arrangement_type: self.arrangement_type.into(),
|
||||
short_name: {
|
||||
let mut label = [0; MAX_SHORT_LABEL as usize];
|
||||
for (b, c) in self.short_name.bytes().zip(label.iter_mut()) {
|
||||
*c = b;
|
||||
}
|
||||
label
|
||||
},
|
||||
future: [0; 48],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::ChannelProperties> for ChannelInfo {
|
||||
fn from(api: api::ChannelProperties) -> ChannelInfo {
|
||||
ChannelInfo {
|
||||
name: String::from_utf8_lossy(&api.name).to_string(),
|
||||
short_name: String::from_utf8_lossy(&api.short_name).to_string(),
|
||||
active: api::ChannelFlags::from_bits(api.flags)
|
||||
.expect("Invalid bits in channel info")
|
||||
.intersects(api::ChannelFlags::ACTIVE),
|
||||
arrangement_type: SpeakerArrangementType::from(api),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Target for Speaker arrangement type. Can be a cinema configuration or music configuration. Both
|
||||
/// are technically identical but this provides extra information to the host.
|
||||
pub enum ArrangementTarget {
|
||||
/// Music arrangement. Technically identical to Cinema.
|
||||
Music,
|
||||
/// Cinematic arrangement. Technically identical to Music.
|
||||
Cinema,
|
||||
}
|
||||
|
||||
/// An enum for all channels in a stereo configuration.
|
||||
pub enum StereoChannel {
|
||||
/// Left channel.
|
||||
Left,
|
||||
/// Right channel.
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Possible stereo speaker configurations.
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum StereoConfig {
|
||||
/// Regular.
|
||||
L_R,
|
||||
/// Left surround, right surround.
|
||||
Ls_Rs,
|
||||
/// Left center, right center.
|
||||
Lc_Rc,
|
||||
/// Side left, side right.
|
||||
Sl_Sr,
|
||||
/// Center, low frequency effects.
|
||||
C_Lfe,
|
||||
}
|
||||
|
||||
/// Possible surround speaker configurations.
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum SurroundConfig {
|
||||
/// 3.0 surround sound.
|
||||
/// Cinema: L R C
|
||||
/// Music: L R S
|
||||
S3_0(ArrangementTarget),
|
||||
/// 3.1 surround sound.
|
||||
/// Cinema: L R C Lfe
|
||||
/// Music: L R Lfe S
|
||||
S3_1(ArrangementTarget),
|
||||
/// 4.0 surround sound.
|
||||
/// Cinema: L R C S (LCRS)
|
||||
/// Music: L R Ls Rs (Quadro)
|
||||
S4_0(ArrangementTarget),
|
||||
/// 4.1 surround sound.
|
||||
/// Cinema: L R C Lfe S (LCRS + Lfe)
|
||||
/// Music: L R Ls Rs (Quadro + Lfe)
|
||||
S4_1(ArrangementTarget),
|
||||
/// 5.0 surround sound.
|
||||
/// Cinema and music: L R C Ls Rs
|
||||
S5_0,
|
||||
/// 5.1 surround sound.
|
||||
/// Cinema and music: L R C Lfe Ls Rs
|
||||
S5_1,
|
||||
/// 6.0 surround sound.
|
||||
/// Cinema: L R C Ls Rs Cs
|
||||
/// Music: L R Ls Rs Sl Sr
|
||||
S6_0(ArrangementTarget),
|
||||
/// 6.1 surround sound.
|
||||
/// Cinema: L R C Lfe Ls Rs Cs
|
||||
/// Music: L R Ls Rs Sl Sr
|
||||
S6_1(ArrangementTarget),
|
||||
/// 7.0 surround sound.
|
||||
/// Cinema: L R C Ls Rs Lc Rc
|
||||
/// Music: L R C Ls Rs Sl Sr
|
||||
S7_0(ArrangementTarget),
|
||||
/// 7.1 surround sound.
|
||||
/// Cinema: L R C Lfe Ls Rs Lc Rc
|
||||
/// Music: L R C Lfe Ls Rs Sl Sr
|
||||
S7_1(ArrangementTarget),
|
||||
/// 8.0 surround sound.
|
||||
/// Cinema: L R C Ls Rs Lc Rc Cs
|
||||
/// Music: L R C Ls Rs Cs Sl Sr
|
||||
S8_0(ArrangementTarget),
|
||||
/// 8.1 surround sound.
|
||||
/// Cinema: L R C Lfe Ls Rs Lc Rc Cs
|
||||
/// Music: L R C Lfe Ls Rs Cs Sl Sr
|
||||
S8_1(ArrangementTarget),
|
||||
/// 10.2 surround sound.
|
||||
/// Cinema + Music: L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
|
||||
S10_2,
|
||||
}
|
||||
|
||||
/// Type representing how a channel is used. Only useful for some hosts.
|
||||
pub enum SpeakerArrangementType {
|
||||
/// Custom arrangement not specified to host.
|
||||
Custom,
|
||||
/// Empty arrangement.
|
||||
Empty,
|
||||
/// Mono channel.
|
||||
Mono,
|
||||
/// Stereo channel. Contains type of stereo arrangement and speaker represented.
|
||||
Stereo(StereoConfig, StereoChannel),
|
||||
/// Surround channel. Contains surround arrangement and target (cinema or music).
|
||||
Surround(SurroundConfig),
|
||||
}
|
||||
|
||||
impl Default for SpeakerArrangementType {
|
||||
fn default() -> SpeakerArrangementType {
|
||||
SpeakerArrangementType::Mono
|
||||
}
|
||||
}
|
||||
|
||||
impl SpeakerArrangementType {
|
||||
/// Determine whether this channel is part of a surround speaker arrangement.
|
||||
pub fn is_speaker_type(&self) -> bool {
|
||||
if let SpeakerArrangementType::Surround(..) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether this channel is the left speaker in a stereo pair.
|
||||
pub fn is_left_stereo(&self) -> bool {
|
||||
if let SpeakerArrangementType::Stereo(_, StereoChannel::Left) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<api::SpeakerArrangementType> for SpeakerArrangementType {
|
||||
/// Convert to VST API arrangement type.
|
||||
fn into(self) -> api::SpeakerArrangementType {
|
||||
use self::ArrangementTarget::{Cinema, Music};
|
||||
use self::SpeakerArrangementType::*;
|
||||
use api::SpeakerArrangementType as Raw;
|
||||
|
||||
match self {
|
||||
Custom => Raw::Custom,
|
||||
Empty => Raw::Empty,
|
||||
Mono => Raw::Mono,
|
||||
Stereo(conf, _) => {
|
||||
match conf {
|
||||
// Stereo channels.
|
||||
StereoConfig::L_R => Raw::Stereo,
|
||||
StereoConfig::Ls_Rs => Raw::StereoSurround,
|
||||
StereoConfig::Lc_Rc => Raw::StereoCenter,
|
||||
StereoConfig::Sl_Sr => Raw::StereoSide,
|
||||
StereoConfig::C_Lfe => Raw::StereoCLfe,
|
||||
}
|
||||
}
|
||||
Surround(conf) => {
|
||||
match conf {
|
||||
// Surround channels.
|
||||
SurroundConfig::S3_0(Music) => Raw::Music30,
|
||||
SurroundConfig::S3_0(Cinema) => Raw::Cinema30,
|
||||
|
||||
SurroundConfig::S3_1(Music) => Raw::Music31,
|
||||
SurroundConfig::S3_1(Cinema) => Raw::Cinema31,
|
||||
|
||||
SurroundConfig::S4_0(Music) => Raw::Music40,
|
||||
SurroundConfig::S4_0(Cinema) => Raw::Cinema40,
|
||||
|
||||
SurroundConfig::S4_1(Music) => Raw::Music41,
|
||||
SurroundConfig::S4_1(Cinema) => Raw::Cinema41,
|
||||
|
||||
SurroundConfig::S5_0 => Raw::Surround50,
|
||||
SurroundConfig::S5_1 => Raw::Surround51,
|
||||
|
||||
SurroundConfig::S6_0(Music) => Raw::Music60,
|
||||
SurroundConfig::S6_0(Cinema) => Raw::Cinema60,
|
||||
|
||||
SurroundConfig::S6_1(Music) => Raw::Music61,
|
||||
SurroundConfig::S6_1(Cinema) => Raw::Cinema61,
|
||||
|
||||
SurroundConfig::S7_0(Music) => Raw::Music70,
|
||||
SurroundConfig::S7_0(Cinema) => Raw::Cinema70,
|
||||
|
||||
SurroundConfig::S7_1(Music) => Raw::Music71,
|
||||
SurroundConfig::S7_1(Cinema) => Raw::Cinema71,
|
||||
|
||||
SurroundConfig::S8_0(Music) => Raw::Music80,
|
||||
SurroundConfig::S8_0(Cinema) => Raw::Cinema80,
|
||||
|
||||
SurroundConfig::S8_1(Music) => Raw::Music81,
|
||||
SurroundConfig::S8_1(Cinema) => Raw::Cinema81,
|
||||
|
||||
SurroundConfig::S10_2 => Raw::Surround102,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the VST API equivalent struct into something more usable.
|
||||
///
|
||||
/// We implement `From<ChannelProperties>` as `SpeakerArrangementType` contains extra info about
|
||||
/// stereo speakers found in the channel flags.
|
||||
impl From<api::ChannelProperties> for SpeakerArrangementType {
|
||||
fn from(api: api::ChannelProperties) -> SpeakerArrangementType {
|
||||
use self::ArrangementTarget::{Cinema, Music};
|
||||
use self::SpeakerArrangementType::*;
|
||||
use self::SurroundConfig::*;
|
||||
use api::SpeakerArrangementType as Raw;
|
||||
|
||||
let stereo = if api::ChannelFlags::from_bits(api.flags)
|
||||
.expect("Invalid Channel Flags")
|
||||
.intersects(api::ChannelFlags::STEREO)
|
||||
{
|
||||
StereoChannel::Left
|
||||
} else {
|
||||
StereoChannel::Right
|
||||
};
|
||||
|
||||
match api.arrangement_type {
|
||||
Raw::Custom => Custom,
|
||||
Raw::Empty => Empty,
|
||||
Raw::Mono => Mono,
|
||||
|
||||
Raw::Stereo => Stereo(StereoConfig::L_R, stereo),
|
||||
Raw::StereoSurround => Stereo(StereoConfig::Ls_Rs, stereo),
|
||||
Raw::StereoCenter => Stereo(StereoConfig::Lc_Rc, stereo),
|
||||
Raw::StereoSide => Stereo(StereoConfig::Sl_Sr, stereo),
|
||||
Raw::StereoCLfe => Stereo(StereoConfig::C_Lfe, stereo),
|
||||
|
||||
Raw::Music30 => Surround(S3_0(Music)),
|
||||
Raw::Cinema30 => Surround(S3_0(Cinema)),
|
||||
|
||||
Raw::Music31 => Surround(S3_1(Music)),
|
||||
Raw::Cinema31 => Surround(S3_1(Cinema)),
|
||||
|
||||
Raw::Music40 => Surround(S4_0(Music)),
|
||||
Raw::Cinema40 => Surround(S4_0(Cinema)),
|
||||
|
||||
Raw::Music41 => Surround(S4_1(Music)),
|
||||
Raw::Cinema41 => Surround(S4_1(Cinema)),
|
||||
|
||||
Raw::Surround50 => Surround(S5_0),
|
||||
Raw::Surround51 => Surround(S5_1),
|
||||
|
||||
Raw::Music60 => Surround(S6_0(Music)),
|
||||
Raw::Cinema60 => Surround(S6_0(Cinema)),
|
||||
|
||||
Raw::Music61 => Surround(S6_1(Music)),
|
||||
Raw::Cinema61 => Surround(S6_1(Cinema)),
|
||||
|
||||
Raw::Music70 => Surround(S7_0(Music)),
|
||||
Raw::Cinema70 => Surround(S7_0(Cinema)),
|
||||
|
||||
Raw::Music71 => Surround(S7_1(Music)),
|
||||
Raw::Cinema71 => Surround(S7_1(Cinema)),
|
||||
|
||||
Raw::Music80 => Surround(S8_0(Music)),
|
||||
Raw::Cinema80 => Surround(S8_0(Cinema)),
|
||||
|
||||
Raw::Music81 => Surround(S8_1(Music)),
|
||||
Raw::Cinema81 => Surround(S8_1(Cinema)),
|
||||
|
||||
Raw::Surround102 => Surround(S10_2),
|
||||
}
|
||||
}
|
||||
}
|
155
plugin/vst/src/editor.rs
Normal file
155
plugin/vst/src/editor.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
//! All VST plugin editor related functionality.
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
/// Implemented by plugin editors.
|
||||
#[allow(unused_variables)]
|
||||
pub trait Editor {
|
||||
/// Get the size of the editor window.
|
||||
fn size(&self) -> (i32, i32);
|
||||
|
||||
/// Get the coordinates of the editor window.
|
||||
fn position(&self) -> (i32, i32);
|
||||
|
||||
/// Editor idle call. Called by host.
|
||||
fn idle(&mut self) {}
|
||||
|
||||
/// Called when the editor window is closed.
|
||||
fn close(&mut self) {}
|
||||
|
||||
/// Called when the editor window is opened.
|
||||
///
|
||||
/// `parent` is a window pointer that the new window should attach itself to.
|
||||
/// **It is dependent upon the platform you are targeting.**
|
||||
///
|
||||
/// A few examples:
|
||||
///
|
||||
/// - On Windows, it should be interpreted as a `HWND`
|
||||
/// - On Mac OS X (64 bit), it should be interpreted as a `NSView*`
|
||||
/// - On X11 platforms, it should be interpreted as a `u32` (the ID number of the parent window)
|
||||
///
|
||||
/// Return `true` if the window opened successfully, `false` otherwise.
|
||||
fn open(&mut self, parent: *mut c_void) -> bool;
|
||||
|
||||
/// Return whether the window is currently open.
|
||||
fn is_open(&mut self) -> bool;
|
||||
|
||||
/// Set the knob mode for this editor (if supported by host).
|
||||
///
|
||||
/// Return `true` if the knob mode was set.
|
||||
fn set_knob_mode(&mut self, mode: KnobMode) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Receive key up event. Return `true` if the key was used.
|
||||
fn key_up(&mut self, keycode: KeyCode) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Receive key down event. Return `true` if the key was used.
|
||||
fn key_down(&mut self, keycode: KeyCode) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle used to specify dimensions of editor window.
|
||||
#[doc(hidden)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Rect {
|
||||
/// Y value in pixels of top side.
|
||||
pub top: i16,
|
||||
/// X value in pixels of left side.
|
||||
pub left: i16,
|
||||
/// Y value in pixels of bottom side.
|
||||
pub bottom: i16,
|
||||
/// X value in pixels of right side.
|
||||
pub right: i16,
|
||||
}
|
||||
|
||||
/// A platform independent key code. Includes modifier keys.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct KeyCode {
|
||||
/// ASCII character for key pressed (if applicable).
|
||||
pub character: char,
|
||||
/// Key pressed. See `enums::Key`.
|
||||
pub key: Key,
|
||||
/// Modifier key bitflags. See `enums::flags::modifier_key`.
|
||||
pub modifier: u8,
|
||||
}
|
||||
|
||||
/// Allows host to set how a parameter knob works.
|
||||
#[repr(isize)]
|
||||
#[derive(Copy, Clone, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum KnobMode {
|
||||
Circular,
|
||||
CircularRelative,
|
||||
Linear,
|
||||
}
|
||||
|
||||
/// Platform independent key codes.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(isize)]
|
||||
#[derive(Debug, Copy, Clone, TryFromPrimitive, IntoPrimitive)]
|
||||
pub enum Key {
|
||||
None = 0,
|
||||
Back,
|
||||
Tab,
|
||||
Clear,
|
||||
Return,
|
||||
Pause,
|
||||
Escape,
|
||||
Space,
|
||||
Next,
|
||||
End,
|
||||
Home,
|
||||
Left,
|
||||
Up,
|
||||
Right,
|
||||
Down,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Select,
|
||||
Print,
|
||||
Enter,
|
||||
Snapshot,
|
||||
Insert,
|
||||
Delete,
|
||||
Help,
|
||||
Numpad0,
|
||||
Numpad1,
|
||||
Numpad2,
|
||||
Numpad3,
|
||||
Numpad4,
|
||||
Numpad5,
|
||||
Numpad6,
|
||||
Numpad7,
|
||||
Numpad8,
|
||||
Numpad9,
|
||||
Multiply,
|
||||
Add,
|
||||
Separator,
|
||||
Subtract,
|
||||
Decimal,
|
||||
Divide,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
Numlock,
|
||||
Scroll,
|
||||
Shift,
|
||||
Control,
|
||||
Alt,
|
||||
Equals,
|
||||
}
|
133
plugin/vst/src/event.rs
Normal file
133
plugin/vst/src/event.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
//! Interfaces to VST events.
|
||||
// TODO: Update and explain both host and plugin events
|
||||
|
||||
use std::{mem, slice};
|
||||
|
||||
use crate::api;
|
||||
|
||||
/// A VST event.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Event<'a> {
|
||||
/// A midi event.
|
||||
///
|
||||
/// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is
|
||||
/// called.
|
||||
Midi(MidiEvent),
|
||||
|
||||
/// A system exclusive event.
|
||||
///
|
||||
/// This is just a block of data and it is up to the plugin to interpret this. Generally used
|
||||
/// by midi controllers.
|
||||
SysEx(SysExEvent<'a>),
|
||||
|
||||
/// A deprecated event.
|
||||
///
|
||||
/// Passes the raw midi event structure along with this so that implementors can handle
|
||||
/// optionally handle this event.
|
||||
Deprecated(api::Event),
|
||||
}
|
||||
|
||||
/// A midi event.
|
||||
///
|
||||
/// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is
|
||||
/// called.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct MidiEvent {
|
||||
/// The raw midi data associated with this event.
|
||||
pub data: [u8; 3],
|
||||
|
||||
/// Number of samples into the current processing block that this event occurs on.
|
||||
///
|
||||
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||
/// `samples[123]`.
|
||||
// TODO: Don't repeat this value in all event types
|
||||
pub delta_frames: i32,
|
||||
|
||||
/// This midi event was created live as opposed to being played back in the sequencer.
|
||||
///
|
||||
/// This can give the plugin priority over this event if it introduces a lot of latency.
|
||||
pub live: bool,
|
||||
|
||||
/// The length of the midi note associated with this event, if available.
|
||||
pub note_length: Option<i32>,
|
||||
|
||||
/// Offset in samples into note from note start, if available.
|
||||
pub note_offset: Option<i32>,
|
||||
|
||||
/// Detuning between -63 and +64 cents.
|
||||
pub detune: i8,
|
||||
|
||||
/// Note off velocity between 0 and 127.
|
||||
pub note_off_velocity: u8,
|
||||
}
|
||||
|
||||
/// A system exclusive event.
|
||||
///
|
||||
/// This is just a block of data and it is up to the plugin to interpret this. Generally used
|
||||
/// by midi controllers.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct SysExEvent<'a> {
|
||||
/// The SysEx payload.
|
||||
pub payload: &'a [u8],
|
||||
|
||||
/// Number of samples into the current processing block that this event occurs on.
|
||||
///
|
||||
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||
/// `samples[123]`.
|
||||
pub delta_frames: i32,
|
||||
}
|
||||
|
||||
impl<'a> Event<'a> {
|
||||
/// Creates a high-level event from the given low-level API event.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// You must ensure that the given pointer refers to a valid event of the correct event type.
|
||||
/// For example, if the event type is [`api::EventType::SysEx`], it should point to a
|
||||
/// [`SysExEvent`]. In case of a [`SysExEvent`], `system_data` and `data_size` must be correct.
|
||||
pub unsafe fn from_raw_event(event: *const api::Event) -> Event<'a> {
|
||||
use api::EventType::*;
|
||||
let event = &*event;
|
||||
match event.event_type {
|
||||
Midi => {
|
||||
let event: api::MidiEvent = mem::transmute(*event);
|
||||
|
||||
let length = if event.note_length > 0 {
|
||||
Some(event.note_length)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let offset = if event.note_offset > 0 {
|
||||
Some(event.note_offset)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let flags = api::MidiEventFlags::from_bits(event.flags).unwrap();
|
||||
|
||||
Event::Midi(MidiEvent {
|
||||
data: event.midi_data,
|
||||
delta_frames: event.delta_frames,
|
||||
live: flags.intersects(api::MidiEventFlags::REALTIME_EVENT),
|
||||
note_length: length,
|
||||
note_offset: offset,
|
||||
detune: event.detune,
|
||||
note_off_velocity: event.note_off_velocity,
|
||||
})
|
||||
}
|
||||
|
||||
SysEx => Event::SysEx(SysExEvent {
|
||||
payload: {
|
||||
// We can safely cast the event pointer to a `SysExEvent` pointer as
|
||||
// event_type refers to a `SysEx` type.
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let event: &api::SysExEvent = &*(event as *const api::Event as *const api::SysExEvent);
|
||||
slice::from_raw_parts(event.system_data, event.data_size as usize)
|
||||
},
|
||||
|
||||
delta_frames: event.delta_frames,
|
||||
}),
|
||||
|
||||
_ => Event::Deprecated(*event),
|
||||
}
|
||||
}
|
||||
}
|
962
plugin/vst/src/host.rs
Normal file
962
plugin/vst/src/host.rs
Normal file
|
@ -0,0 +1,962 @@
|
|||
//! Host specific structures.
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use num_traits::Float;
|
||||
|
||||
use libloading::Library;
|
||||
use std::cell::UnsafeCell;
|
||||
use std::convert::TryFrom;
|
||||
use std::error::Error;
|
||||
use std::ffi::CString;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::raw::c_void;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{fmt, ptr, slice};
|
||||
|
||||
use crate::{
|
||||
api::{self, consts::*, AEffect, PluginFlags, PluginMain, Supported, TimeInfo},
|
||||
buffer::AudioBuffer,
|
||||
channels::ChannelInfo,
|
||||
editor::{Editor, Rect},
|
||||
interfaces,
|
||||
plugin::{self, Category, HostCallback, Info, Plugin, PluginParameters},
|
||||
};
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[doc(hidden)]
|
||||
pub enum OpCode {
|
||||
/// [index]: parameter index
|
||||
/// [opt]: parameter value
|
||||
Automate = 0,
|
||||
/// [return]: host vst version (e.g. 2400 for VST 2.4)
|
||||
Version,
|
||||
/// [return]: current plugin ID (useful for shell plugins to figure out which plugin to load in
|
||||
/// `VSTPluginMain()`).
|
||||
CurrentId,
|
||||
/// No arguments. Give idle time to Host application, e.g. if plug-in editor is doing mouse
|
||||
/// tracking in a modal loop.
|
||||
Idle,
|
||||
/// Deprecated.
|
||||
_PinConnected = 4,
|
||||
|
||||
/// Deprecated.
|
||||
_WantMidi = 6, // Not a typo
|
||||
/// [value]: request mask. see `VstTimeInfoFlags`
|
||||
/// [return]: `VstTimeInfo` pointer or null if not supported.
|
||||
GetTime,
|
||||
/// Inform host that the plugin has MIDI events ready to be processed. Should be called at the
|
||||
/// end of `Plugin::process`.
|
||||
/// [ptr]: `VstEvents*` the events to be processed.
|
||||
/// [return]: 1 if supported and processed OK.
|
||||
ProcessEvents,
|
||||
/// Deprecated.
|
||||
_SetTime,
|
||||
/// Deprecated.
|
||||
_TempoAt,
|
||||
/// Deprecated.
|
||||
_GetNumAutomatableParameters,
|
||||
/// Deprecated.
|
||||
_GetParameterQuantization,
|
||||
|
||||
/// Notifies the host that the input/output setup has changed. This can allow the host to check
|
||||
/// numInputs/numOutputs or call `getSpeakerArrangement()`.
|
||||
/// [return]: 1 if supported.
|
||||
IOChanged,
|
||||
|
||||
/// Deprecated.
|
||||
_NeedIdle,
|
||||
|
||||
/// Request the host to resize the plugin window.
|
||||
/// [index]: new width.
|
||||
/// [value]: new height.
|
||||
SizeWindow,
|
||||
/// [return]: the current sample rate.
|
||||
GetSampleRate,
|
||||
/// [return]: the current block size.
|
||||
GetBlockSize,
|
||||
/// [return]: the input latency in samples.
|
||||
GetInputLatency,
|
||||
/// [return]: the output latency in samples.
|
||||
GetOutputLatency,
|
||||
|
||||
/// Deprecated.
|
||||
_GetPreviousPlug,
|
||||
/// Deprecated.
|
||||
_GetNextPlug,
|
||||
/// Deprecated.
|
||||
_WillReplaceOrAccumulate,
|
||||
|
||||
/// [return]: the current process level, see `VstProcessLevels`
|
||||
GetCurrentProcessLevel,
|
||||
/// [return]: the current automation state, see `VstAutomationStates`
|
||||
GetAutomationState,
|
||||
|
||||
/// The plugin is ready to begin offline processing.
|
||||
/// [index]: number of new audio files.
|
||||
/// [value]: number of audio files.
|
||||
/// [ptr]: `AudioFile*` the host audio files. Flags can be updated from plugin.
|
||||
OfflineStart,
|
||||
/// Called by the plugin to read data.
|
||||
/// [index]: (bool)
|
||||
/// VST offline processing allows a plugin to overwrite existing files. If this value is
|
||||
/// true then the host will read the original file's samples, but if it is false it will
|
||||
/// read the samples which the plugin has written via `OfflineWrite`
|
||||
/// [value]: see `OfflineOption`
|
||||
/// [ptr]: `OfflineTask*` describing the task.
|
||||
/// [return]: 1 on success
|
||||
OfflineRead,
|
||||
/// Called by the plugin to write data.
|
||||
/// [value]: see `OfflineOption`
|
||||
/// [ptr]: `OfflineTask*` describing the task.
|
||||
OfflineWrite,
|
||||
/// Unknown. Used in offline processing.
|
||||
OfflineGetCurrentPass,
|
||||
/// Unknown. Used in offline processing.
|
||||
OfflineGetCurrentMetaPass,
|
||||
|
||||
/// Deprecated.
|
||||
_SetOutputSampleRate,
|
||||
/// Deprecated.
|
||||
_GetOutputSpeakerArrangement,
|
||||
|
||||
/// Get the vendor string.
|
||||
/// [ptr]: `char*` for vendor string, limited to `MAX_VENDOR_STR_LEN`.
|
||||
GetVendorString,
|
||||
/// Get the product string.
|
||||
/// [ptr]: `char*` for vendor string, limited to `MAX_PRODUCT_STR_LEN`.
|
||||
GetProductString,
|
||||
/// [return]: vendor-specific version
|
||||
GetVendorVersion,
|
||||
/// Vendor specific handling.
|
||||
VendorSpecific,
|
||||
|
||||
/// Deprecated.
|
||||
_SetIcon,
|
||||
|
||||
/// Check if the host supports a feature.
|
||||
/// [ptr]: `char*` can do string
|
||||
/// [return]: 1 if supported
|
||||
CanDo,
|
||||
/// Get the language of the host.
|
||||
/// [return]: `VstHostLanguage`
|
||||
GetLanguage,
|
||||
|
||||
/// Deprecated.
|
||||
_OpenWindow,
|
||||
/// Deprecated.
|
||||
_CloseWindow,
|
||||
|
||||
/// Get the current directory.
|
||||
/// [return]: `FSSpec` on OS X, `char*` otherwise
|
||||
GetDirectory,
|
||||
/// Tell the host that the plugin's parameters have changed, refresh the UI.
|
||||
///
|
||||
/// No arguments.
|
||||
UpdateDisplay,
|
||||
/// Tell the host that if needed, it should record automation data for a control.
|
||||
///
|
||||
/// Typically called when the plugin editor begins changing a control.
|
||||
///
|
||||
/// [index]: index of the control.
|
||||
/// [return]: true on success.
|
||||
BeginEdit,
|
||||
/// A control is no longer being changed.
|
||||
///
|
||||
/// Typically called after the plugin editor is done.
|
||||
///
|
||||
/// [index]: index of the control.
|
||||
/// [return]: true on success.
|
||||
EndEdit,
|
||||
/// Open the host file selector.
|
||||
/// [ptr]: `VstFileSelect*`
|
||||
/// [return]: true on success.
|
||||
OpenFileSelector,
|
||||
/// Close the host file selector.
|
||||
/// [ptr]: `VstFileSelect*`
|
||||
/// [return]: true on success.
|
||||
CloseFileSelector,
|
||||
|
||||
/// Deprecated.
|
||||
_EditFile,
|
||||
/// Deprecated.
|
||||
/// [ptr]: char[2048] or sizeof (FSSpec).
|
||||
/// [return]: 1 if supported.
|
||||
_GetChunkFile,
|
||||
/// Deprecated.
|
||||
_GetInputSpeakerArrangement,
|
||||
}
|
||||
|
||||
/// Implemented by all VST hosts.
|
||||
#[allow(unused_variables)]
|
||||
pub trait Host {
|
||||
/// Automate a parameter; the value has been changed.
|
||||
fn automate(&self, index: i32, value: f32) {}
|
||||
|
||||
/// Signal that automation of a parameter started (the knob has been touched / mouse button down).
|
||||
fn begin_edit(&self, index: i32) {}
|
||||
|
||||
/// Signal that automation of a parameter ended (the knob is no longer been touched / mouse button up).
|
||||
fn end_edit(&self, index: i32) {}
|
||||
|
||||
/// Get the plugin ID of the currently loading plugin.
|
||||
///
|
||||
/// This is only useful for shell plugins where this value will change the plugin returned.
|
||||
/// `TODO: implement shell plugins`
|
||||
fn get_plugin_id(&self) -> i32 {
|
||||
// TODO: Handle this properly
|
||||
0
|
||||
}
|
||||
|
||||
/// An idle call.
|
||||
///
|
||||
/// This is useful when the plugin is doing something such as mouse tracking in the UI.
|
||||
fn idle(&self) {}
|
||||
|
||||
/// Get vendor and product information.
|
||||
///
|
||||
/// Returns a tuple in the form of `(version, vendor_name, product_name)`.
|
||||
fn get_info(&self) -> (isize, String, String) {
|
||||
(1, "vendor string".to_owned(), "product string".to_owned())
|
||||
}
|
||||
|
||||
/// Handle incoming events from the plugin.
|
||||
fn process_events(&self, events: &api::Events) {}
|
||||
|
||||
/// Get time information.
|
||||
fn get_time_info(&self, mask: i32) -> Option<TimeInfo> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get block size.
|
||||
fn get_block_size(&self) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
/// Refresh UI after the plugin's parameters changed.
|
||||
///
|
||||
/// Note: some hosts will call some `PluginParameters` methods from within the `update_display`
|
||||
/// call, including `get_parameter`, `get_parameter_label`, `get_parameter_name`
|
||||
/// and `get_parameter_text`.
|
||||
fn update_display(&self) {}
|
||||
}
|
||||
|
||||
/// All possible errors that can occur when loading a VST plugin.
|
||||
#[derive(Debug)]
|
||||
pub enum PluginLoadError {
|
||||
/// Could not load given path.
|
||||
InvalidPath,
|
||||
|
||||
/// Given path is not a VST plugin.
|
||||
NotAPlugin,
|
||||
|
||||
/// Failed to create an instance of this plugin.
|
||||
///
|
||||
/// This can happen for many reasons, such as if the plugin requires a different version of
|
||||
/// the VST API to be used, or due to improper licensing.
|
||||
InstanceFailed,
|
||||
|
||||
/// The API version which the plugin used is not supported by this library.
|
||||
InvalidApiVersion,
|
||||
}
|
||||
|
||||
impl fmt::Display for PluginLoadError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::PluginLoadError::*;
|
||||
let description = match self {
|
||||
InvalidPath => "Could not open the requested path",
|
||||
NotAPlugin => "The given path does not contain a VST2.4 compatible library",
|
||||
InstanceFailed => "Failed to create a plugin instance",
|
||||
InvalidApiVersion => "The plugin API version is not compatible with this library",
|
||||
};
|
||||
write!(f, "{}", description)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PluginLoadError {}
|
||||
|
||||
/// Wrapper for an externally loaded VST plugin.
|
||||
///
|
||||
/// The only functionality this struct provides is loading plugins, which can be done via the
|
||||
/// [`load`](#method.load) method.
|
||||
pub struct PluginLoader<T: Host> {
|
||||
main: PluginMain,
|
||||
lib: Arc<Library>,
|
||||
host: Arc<Mutex<T>>,
|
||||
}
|
||||
|
||||
/// An instance of an externally loaded VST plugin.
|
||||
#[allow(dead_code)] // To keep `lib` around.
|
||||
pub struct PluginInstance {
|
||||
params: Arc<PluginParametersInstance>,
|
||||
lib: Arc<Library>,
|
||||
info: Info,
|
||||
is_editor_active: bool,
|
||||
}
|
||||
|
||||
struct PluginParametersInstance {
|
||||
effect: UnsafeCell<*mut AEffect>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PluginParametersInstance {}
|
||||
unsafe impl Sync for PluginParametersInstance {}
|
||||
|
||||
impl Drop for PluginInstance {
|
||||
fn drop(&mut self) {
|
||||
self.dispatch(plugin::OpCode::Shutdown, 0, 0, ptr::null_mut(), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The editor of an externally loaded VST plugin.
|
||||
struct EditorInstance {
|
||||
params: Arc<PluginParametersInstance>,
|
||||
is_open: bool,
|
||||
}
|
||||
|
||||
impl EditorInstance {
|
||||
fn get_rect(&self) -> Option<Rect> {
|
||||
let mut rect: *mut Rect = std::ptr::null_mut();
|
||||
let rect_ptr: *mut *mut Rect = &mut rect;
|
||||
|
||||
let result = self
|
||||
.params
|
||||
.dispatch(plugin::OpCode::EditorGetRect, 0, 0, rect_ptr as *mut c_void, 0.0);
|
||||
|
||||
if result == 0 || rect.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { *rect }) // TODO: Who owns rect? Who should free the memory?
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for EditorInstance {
|
||||
fn size(&self) -> (i32, i32) {
|
||||
// Assuming coordinate origins from top-left
|
||||
match self.get_rect() {
|
||||
None => (0, 0),
|
||||
Some(rect) => ((rect.right - rect.left) as i32, (rect.bottom - rect.top) as i32),
|
||||
}
|
||||
}
|
||||
|
||||
fn position(&self) -> (i32, i32) {
|
||||
// Assuming coordinate origins from top-left
|
||||
match self.get_rect() {
|
||||
None => (0, 0),
|
||||
Some(rect) => (rect.left as i32, rect.top as i32),
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.params
|
||||
.dispatch(plugin::OpCode::EditorClose, 0, 0, ptr::null_mut(), 0.0);
|
||||
self.is_open = false;
|
||||
}
|
||||
|
||||
fn open(&mut self, parent: *mut c_void) -> bool {
|
||||
let result = self.params.dispatch(plugin::OpCode::EditorOpen, 0, 0, parent, 0.0);
|
||||
|
||||
let opened = result == 1;
|
||||
if opened {
|
||||
self.is_open = true;
|
||||
}
|
||||
|
||||
opened
|
||||
}
|
||||
|
||||
fn is_open(&mut self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Host> PluginLoader<T> {
|
||||
/// Load a plugin at the given path with the given host.
|
||||
///
|
||||
/// Because of the possibility of multi-threading problems that can occur when using plugins,
|
||||
/// the host must be passed in via an `Arc<Mutex<T>>` object. This makes sure that even if the
|
||||
/// plugins are multi-threaded no data race issues can occur.
|
||||
///
|
||||
/// Upon success, this method returns a [`PluginLoader`](.) object which you can use to call
|
||||
/// [`instance`](#method.instance) to create a new instance of the plugin.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::path::Path;
|
||||
/// # use std::sync::{Arc, Mutex};
|
||||
/// # use vst::host::{Host, PluginLoader};
|
||||
/// # let path = Path::new(".");
|
||||
/// # struct MyHost;
|
||||
/// # impl MyHost { fn new() -> MyHost { MyHost } }
|
||||
/// # impl Host for MyHost {
|
||||
/// # fn automate(&self, _: i32, _: f32) {}
|
||||
/// # fn get_plugin_id(&self) -> i32 { 0 }
|
||||
/// # }
|
||||
/// // ...
|
||||
/// let host = Arc::new(Mutex::new(MyHost::new()));
|
||||
///
|
||||
/// let mut plugin = PluginLoader::load(path, host.clone()).unwrap();
|
||||
///
|
||||
/// let instance = plugin.instance().unwrap();
|
||||
/// // ...
|
||||
/// ```
|
||||
///
|
||||
/// # Linux/Windows
|
||||
/// * This should be a path to the library, typically ending in `.so`/`.dll`.
|
||||
/// * Possible full path: `/home/overdrivenpotato/.vst/u-he/Zebra2.64.so`
|
||||
/// * Possible full path: `C:\Program Files (x86)\VSTPlugins\iZotope Ozone 5.dll`
|
||||
///
|
||||
/// # OS X
|
||||
/// * This should point to the mach-o file within the `.vst` bundle.
|
||||
/// * Plugin: `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst`
|
||||
/// * Possible full path:
|
||||
/// `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst/Contents/MacOS/PluginHooksVST`
|
||||
pub fn load(path: &Path, host: Arc<Mutex<T>>) -> Result<PluginLoader<T>, PluginLoadError> {
|
||||
// Try loading the library at the given path
|
||||
unsafe {
|
||||
let lib = match Library::new(path) {
|
||||
Ok(l) => l,
|
||||
Err(_) => return Err(PluginLoadError::InvalidPath),
|
||||
};
|
||||
|
||||
Ok(PluginLoader {
|
||||
main:
|
||||
// Search the library for the VSTAPI entry point
|
||||
match lib.get(b"VSTPluginMain") {
|
||||
Ok(s) => *s,
|
||||
_ => return Err(PluginLoadError::NotAPlugin),
|
||||
}
|
||||
,
|
||||
lib: Arc::new(lib),
|
||||
host,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the VST entry point and retrieve a (possibly null) pointer.
|
||||
unsafe fn call_main(&mut self) -> *mut AEffect {
|
||||
LOAD_POINTER = Box::into_raw(Box::new(Arc::clone(&self.host))) as *mut c_void;
|
||||
(self.main)(callback_wrapper::<T>)
|
||||
}
|
||||
|
||||
/// Try to create an instance of this VST plugin.
|
||||
///
|
||||
/// If the instance is successfully created, a [`PluginInstance`](struct.PluginInstance.html)
|
||||
/// is returned. This struct implements the [`Plugin` trait](../plugin/trait.Plugin.html).
|
||||
pub fn instance(&mut self) -> Result<PluginInstance, PluginLoadError> {
|
||||
// Call the plugin main function. This also passes the plugin main function as the closure
|
||||
// could not return an error if the symbol wasn't found
|
||||
let effect = unsafe { self.call_main() };
|
||||
|
||||
if effect.is_null() {
|
||||
return Err(PluginLoadError::InstanceFailed);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Move the host to the heap and add it to the `AEffect` struct for future reference
|
||||
(*effect).reserved1 = Box::into_raw(Box::new(Arc::clone(&self.host))) as isize;
|
||||
}
|
||||
|
||||
let instance = PluginInstance::new(effect, Arc::clone(&self.lib));
|
||||
|
||||
let api_ver = instance.dispatch(plugin::OpCode::GetApiVersion, 0, 0, ptr::null_mut(), 0.0);
|
||||
if api_ver >= 2400 {
|
||||
Ok(instance)
|
||||
} else {
|
||||
trace!("Could not load plugin with api version {}", api_ver);
|
||||
Err(PluginLoadError::InvalidApiVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginInstance {
|
||||
fn new(effect: *mut AEffect, lib: Arc<Library>) -> PluginInstance {
|
||||
use plugin::OpCode as op;
|
||||
|
||||
let params = Arc::new(PluginParametersInstance {
|
||||
effect: UnsafeCell::new(effect),
|
||||
});
|
||||
let mut plug = PluginInstance {
|
||||
params,
|
||||
lib,
|
||||
info: Default::default(),
|
||||
is_editor_active: false,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let effect: &AEffect = &*effect;
|
||||
let flags = PluginFlags::from_bits_truncate(effect.flags);
|
||||
|
||||
plug.info = Info {
|
||||
name: plug.read_string(op::GetProductName, MAX_PRODUCT_STR_LEN),
|
||||
vendor: plug.read_string(op::GetVendorName, MAX_VENDOR_STR_LEN),
|
||||
|
||||
presets: effect.numPrograms,
|
||||
parameters: effect.numParams,
|
||||
inputs: effect.numInputs,
|
||||
outputs: effect.numOutputs,
|
||||
|
||||
midi_inputs: 0,
|
||||
midi_outputs: 0,
|
||||
|
||||
unique_id: effect.uniqueId,
|
||||
version: effect.version,
|
||||
|
||||
category: Category::try_from(plug.opcode(op::GetCategory)).unwrap_or(Category::Unknown),
|
||||
|
||||
initial_delay: effect.initialDelay,
|
||||
|
||||
preset_chunks: flags.intersects(PluginFlags::PROGRAM_CHUNKS),
|
||||
f64_precision: flags.intersects(PluginFlags::CAN_DOUBLE_REPLACING),
|
||||
silent_when_stopped: flags.intersects(PluginFlags::NO_SOUND_IN_STOP),
|
||||
};
|
||||
}
|
||||
|
||||
plug
|
||||
}
|
||||
}
|
||||
|
||||
trait Dispatch {
|
||||
fn get_effect(&self) -> *mut AEffect;
|
||||
|
||||
/// Send a dispatch message to the plugin.
|
||||
fn dispatch(&self, opcode: plugin::OpCode, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
|
||||
let dispatcher = unsafe { (*self.get_effect()).dispatcher };
|
||||
if (dispatcher as *mut u8).is_null() {
|
||||
panic!("Plugin was not loaded correctly.");
|
||||
}
|
||||
dispatcher(self.get_effect(), opcode.into(), index, value, ptr, opt)
|
||||
}
|
||||
|
||||
/// Send a lone opcode with no parameters.
|
||||
fn opcode(&self, opcode: plugin::OpCode) -> isize {
|
||||
self.dispatch(opcode, 0, 0, ptr::null_mut(), 0.0)
|
||||
}
|
||||
|
||||
/// Like `dispatch`, except takes a `&str` to send via `ptr`.
|
||||
fn write_string(&self, opcode: plugin::OpCode, index: i32, value: isize, string: &str, opt: f32) -> isize {
|
||||
let string = CString::new(string).expect("Invalid string data");
|
||||
self.dispatch(opcode, index, value, string.as_bytes().as_ptr() as *mut c_void, opt)
|
||||
}
|
||||
|
||||
fn read_string(&self, opcode: plugin::OpCode, max: usize) -> String {
|
||||
self.read_string_param(opcode, 0, 0, 0.0, max)
|
||||
}
|
||||
|
||||
fn read_string_param(&self, opcode: plugin::OpCode, index: i32, value: isize, opt: f32, max: usize) -> String {
|
||||
let mut buf = vec![0; max];
|
||||
self.dispatch(opcode, index, value, buf.as_mut_ptr() as *mut c_void, opt);
|
||||
String::from_utf8_lossy(&buf)
|
||||
.chars()
|
||||
.take_while(|c| *c != '\0')
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch for PluginInstance {
|
||||
fn get_effect(&self) -> *mut AEffect {
|
||||
self.params.get_effect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch for PluginParametersInstance {
|
||||
fn get_effect(&self) -> *mut AEffect {
|
||||
unsafe { *self.effect.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for PluginInstance {
|
||||
fn get_info(&self) -> plugin::Info {
|
||||
self.info.clone()
|
||||
}
|
||||
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
// Plugin::new is only called on client side and PluginInstance is only used on host side
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
self.opcode(plugin::OpCode::Initialize);
|
||||
}
|
||||
|
||||
fn set_sample_rate(&mut self, rate: f32) {
|
||||
self.dispatch(plugin::OpCode::SetSampleRate, 0, 0, ptr::null_mut(), rate);
|
||||
}
|
||||
|
||||
fn set_block_size(&mut self, size: i64) {
|
||||
self.dispatch(plugin::OpCode::SetBlockSize, 0, size as isize, ptr::null_mut(), 0.0);
|
||||
}
|
||||
|
||||
fn resume(&mut self) {
|
||||
self.dispatch(plugin::OpCode::StateChanged, 0, 1, ptr::null_mut(), 0.0);
|
||||
}
|
||||
|
||||
fn suspend(&mut self) {
|
||||
self.dispatch(plugin::OpCode::StateChanged, 0, 0, ptr::null_mut(), 0.0);
|
||||
}
|
||||
|
||||
fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
|
||||
self.dispatch(plugin::OpCode::VendorSpecific, index, value, ptr, opt)
|
||||
}
|
||||
|
||||
fn can_do(&self, can_do: plugin::CanDo) -> Supported {
|
||||
let s: String = can_do.into();
|
||||
Supported::from(self.write_string(plugin::OpCode::CanDo, 0, 0, &s, 0.0))
|
||||
.expect("Invalid response received when querying plugin CanDo")
|
||||
}
|
||||
|
||||
fn get_tail_size(&self) -> isize {
|
||||
self.opcode(plugin::OpCode::GetTailSize)
|
||||
}
|
||||
|
||||
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||
if buffer.input_count() < self.info.inputs as usize {
|
||||
panic!("Too few inputs in AudioBuffer");
|
||||
}
|
||||
if buffer.output_count() < self.info.outputs as usize {
|
||||
panic!("Too few outputs in AudioBuffer");
|
||||
}
|
||||
unsafe {
|
||||
((*self.get_effect()).processReplacing)(
|
||||
self.get_effect(),
|
||||
buffer.raw_inputs().as_ptr() as *const *const _,
|
||||
buffer.raw_outputs().as_mut_ptr() as *mut *mut _,
|
||||
buffer.samples() as i32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
|
||||
if buffer.input_count() < self.info.inputs as usize {
|
||||
panic!("Too few inputs in AudioBuffer");
|
||||
}
|
||||
if buffer.output_count() < self.info.outputs as usize {
|
||||
panic!("Too few outputs in AudioBuffer");
|
||||
}
|
||||
unsafe {
|
||||
((*self.get_effect()).processReplacingF64)(
|
||||
self.get_effect(),
|
||||
buffer.raw_inputs().as_ptr() as *const *const _,
|
||||
buffer.raw_outputs().as_mut_ptr() as *mut *mut _,
|
||||
buffer.samples() as i32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_events(&mut self, events: &api::Events) {
|
||||
self.dispatch(plugin::OpCode::ProcessEvents, 0, 0, events as *const _ as *mut _, 0.0);
|
||||
}
|
||||
|
||||
fn get_input_info(&self, input: i32) -> ChannelInfo {
|
||||
let mut props: MaybeUninit<api::ChannelProperties> = MaybeUninit::uninit();
|
||||
let ptr = props.as_mut_ptr() as *mut c_void;
|
||||
|
||||
self.dispatch(plugin::OpCode::GetInputInfo, input, 0, ptr, 0.0);
|
||||
|
||||
ChannelInfo::from(unsafe { props.assume_init() })
|
||||
}
|
||||
|
||||
fn get_output_info(&self, output: i32) -> ChannelInfo {
|
||||
let mut props: MaybeUninit<api::ChannelProperties> = MaybeUninit::uninit();
|
||||
let ptr = props.as_mut_ptr() as *mut c_void;
|
||||
|
||||
self.dispatch(plugin::OpCode::GetOutputInfo, output, 0, ptr, 0.0);
|
||||
|
||||
ChannelInfo::from(unsafe { props.assume_init() })
|
||||
}
|
||||
|
||||
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||
}
|
||||
|
||||
fn get_editor(&mut self) -> Option<Box<dyn Editor>> {
|
||||
if self.is_editor_active {
|
||||
// An editor is already active, the caller should be using the active editor instead of
|
||||
// requesting for a new one.
|
||||
return None;
|
||||
}
|
||||
|
||||
self.is_editor_active = true;
|
||||
Some(Box::new(EditorInstance {
|
||||
params: self.params.clone(),
|
||||
is_open: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginParameters for PluginParametersInstance {
|
||||
fn change_preset(&self, preset: i32) {
|
||||
self.dispatch(plugin::OpCode::ChangePreset, 0, preset as isize, ptr::null_mut(), 0.0);
|
||||
}
|
||||
|
||||
fn get_preset_num(&self) -> i32 {
|
||||
self.opcode(plugin::OpCode::GetCurrentPresetNum) as i32
|
||||
}
|
||||
|
||||
fn set_preset_name(&self, name: String) {
|
||||
self.write_string(plugin::OpCode::SetCurrentPresetName, 0, 0, &name, 0.0);
|
||||
}
|
||||
|
||||
fn get_preset_name(&self, preset: i32) -> String {
|
||||
self.read_string_param(plugin::OpCode::GetPresetName, preset, 0, 0.0, MAX_PRESET_NAME_LEN)
|
||||
}
|
||||
|
||||
fn get_parameter_label(&self, index: i32) -> String {
|
||||
self.read_string_param(plugin::OpCode::GetParameterLabel, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||
}
|
||||
|
||||
fn get_parameter_text(&self, index: i32) -> String {
|
||||
self.read_string_param(plugin::OpCode::GetParameterDisplay, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||
}
|
||||
|
||||
fn get_parameter_name(&self, index: i32) -> String {
|
||||
self.read_string_param(plugin::OpCode::GetParameterName, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||
}
|
||||
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
unsafe { ((*self.get_effect()).getParameter)(self.get_effect(), index) }
|
||||
}
|
||||
|
||||
fn set_parameter(&self, index: i32, value: f32) {
|
||||
unsafe { ((*self.get_effect()).setParameter)(self.get_effect(), index, value) }
|
||||
}
|
||||
|
||||
fn can_be_automated(&self, index: i32) -> bool {
|
||||
self.dispatch(plugin::OpCode::CanBeAutomated, index, 0, ptr::null_mut(), 0.0) > 0
|
||||
}
|
||||
|
||||
fn string_to_parameter(&self, index: i32, text: String) -> bool {
|
||||
self.write_string(plugin::OpCode::StringToParameter, index, 0, &text, 0.0) > 0
|
||||
}
|
||||
|
||||
// TODO: Editor
|
||||
|
||||
fn get_preset_data(&self) -> Vec<u8> {
|
||||
// Create a pointer that can be updated from the plugin.
|
||||
let mut ptr: *mut u8 = ptr::null_mut();
|
||||
let len = self.dispatch(
|
||||
plugin::OpCode::GetData,
|
||||
1, /*preset*/
|
||||
0,
|
||||
&mut ptr as *mut *mut u8 as *mut c_void,
|
||||
0.0,
|
||||
);
|
||||
let slice = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||
slice.to_vec()
|
||||
}
|
||||
|
||||
fn get_bank_data(&self) -> Vec<u8> {
|
||||
// Create a pointer that can be updated from the plugin.
|
||||
let mut ptr: *mut u8 = ptr::null_mut();
|
||||
let len = self.dispatch(
|
||||
plugin::OpCode::GetData,
|
||||
0, /*bank*/
|
||||
0,
|
||||
&mut ptr as *mut *mut u8 as *mut c_void,
|
||||
0.0,
|
||||
);
|
||||
let slice = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||
slice.to_vec()
|
||||
}
|
||||
|
||||
fn load_preset_data(&self, data: &[u8]) {
|
||||
self.dispatch(
|
||||
plugin::OpCode::SetData,
|
||||
1,
|
||||
data.len() as isize,
|
||||
data.as_ptr() as *mut c_void,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
fn load_bank_data(&self, data: &[u8]) {
|
||||
self.dispatch(
|
||||
plugin::OpCode::SetData,
|
||||
0,
|
||||
data.len() as isize,
|
||||
data.as_ptr() as *mut c_void,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for constructing `AudioBuffer` instances on the host.
|
||||
///
|
||||
/// This struct contains all necessary allocations for an `AudioBuffer` apart
|
||||
/// from the actual sample arrays. This way, the inner processing loop can
|
||||
/// be allocation free even if `AudioBuffer` instances are repeatedly created.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use vst::host::HostBuffer;
|
||||
/// # use vst::plugin::Plugin;
|
||||
/// # fn test<P: Plugin>(plugin: &mut P) {
|
||||
/// let mut host_buffer: HostBuffer<f32> = HostBuffer::new(2, 2);
|
||||
/// let inputs = vec![vec![0.0; 1000]; 2];
|
||||
/// let mut outputs = vec![vec![0.0; 1000]; 2];
|
||||
/// let mut audio_buffer = host_buffer.bind(&inputs, &mut outputs);
|
||||
/// plugin.process(&mut audio_buffer);
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct HostBuffer<T: Float> {
|
||||
inputs: Vec<*const T>,
|
||||
outputs: Vec<*mut T>,
|
||||
}
|
||||
|
||||
impl<T: Float> HostBuffer<T> {
|
||||
/// Create a `HostBuffer` for a given number of input and output channels.
|
||||
pub fn new(input_count: usize, output_count: usize) -> HostBuffer<T> {
|
||||
HostBuffer {
|
||||
inputs: vec![ptr::null(); input_count],
|
||||
outputs: vec![ptr::null_mut(); output_count],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `HostBuffer` for the number of input and output channels
|
||||
/// specified in an `Info` struct.
|
||||
pub fn from_info(info: &Info) -> HostBuffer<T> {
|
||||
HostBuffer::new(info.inputs as usize, info.outputs as usize)
|
||||
}
|
||||
|
||||
/// Bind sample arrays to the `HostBuffer` to create an `AudioBuffer` to pass to a plugin.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function will panic if more inputs or outputs are supplied than the `HostBuffer`
|
||||
/// was created for, or if the sample arrays do not all have the same length.
|
||||
pub fn bind<'a, I, O>(&'a mut self, input_arrays: &[I], output_arrays: &mut [O]) -> AudioBuffer<'a, T>
|
||||
where
|
||||
I: AsRef<[T]> + 'a,
|
||||
O: AsMut<[T]> + 'a,
|
||||
{
|
||||
// Check that number of desired inputs and outputs fit in allocation
|
||||
if input_arrays.len() > self.inputs.len() {
|
||||
panic!("Too many inputs for HostBuffer");
|
||||
}
|
||||
if output_arrays.len() > self.outputs.len() {
|
||||
panic!("Too many outputs for HostBuffer");
|
||||
}
|
||||
|
||||
// Initialize raw pointers and find common length
|
||||
let mut length = None;
|
||||
for (i, input) in input_arrays.iter().map(|r| r.as_ref()).enumerate() {
|
||||
self.inputs[i] = input.as_ptr();
|
||||
match length {
|
||||
None => length = Some(input.len()),
|
||||
Some(old_length) => {
|
||||
if input.len() != old_length {
|
||||
panic!("Mismatching lengths of input arrays");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, output) in output_arrays.iter_mut().map(|r| r.as_mut()).enumerate() {
|
||||
self.outputs[i] = output.as_mut_ptr();
|
||||
match length {
|
||||
None => length = Some(output.len()),
|
||||
Some(old_length) => {
|
||||
if output.len() != old_length {
|
||||
panic!("Mismatching lengths of output arrays");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let length = length.unwrap_or(0);
|
||||
|
||||
// Construct AudioBuffer
|
||||
unsafe {
|
||||
AudioBuffer::from_raw(
|
||||
input_arrays.len(),
|
||||
output_arrays.len(),
|
||||
self.inputs.as_ptr(),
|
||||
self.outputs.as_mut_ptr(),
|
||||
length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of input channels supported by this `HostBuffer`.
|
||||
pub fn input_count(&self) -> usize {
|
||||
self.inputs.len()
|
||||
}
|
||||
|
||||
/// Number of output channels supported by this `HostBuffer`.
|
||||
pub fn output_count(&self) -> usize {
|
||||
self.outputs.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// HACK: a pointer to store the host so that it can be accessed from the `callback_wrapper`
|
||||
/// function passed to the plugin.
|
||||
///
|
||||
/// When the plugin is being loaded, a `Box<Arc<Mutex<T>>>` is transmuted to a `*mut c_void` pointer
|
||||
/// and placed here. When the plugin calls the callback during initialization, the host refers to
|
||||
/// this pointer to get a handle to the Host. After initialization, this pointer is invalidated and
|
||||
/// the host pointer is placed into a [reserved field] in the instance `AEffect` struct.
|
||||
///
|
||||
/// The issue with this approach is that if 2 plugins are simultaneously loaded with 2 different
|
||||
/// host instances, this might fail as one host may receive a pointer to the other one. In practice
|
||||
/// this is a rare situation as you normally won't have 2 separate host instances loading at once.
|
||||
///
|
||||
/// [reserved field]: ../api/struct.AEffect.html#structfield.reserved1
|
||||
static mut LOAD_POINTER: *mut c_void = 0 as *mut c_void;
|
||||
|
||||
/// Function passed to plugin to handle dispatching host opcodes.
|
||||
extern "C" fn callback_wrapper<T: Host>(
|
||||
effect: *mut AEffect,
|
||||
opcode: i32,
|
||||
index: i32,
|
||||
value: isize,
|
||||
ptr: *mut c_void,
|
||||
opt: f32,
|
||||
) -> isize {
|
||||
unsafe {
|
||||
// If the effect pointer is not null and the host pointer is not null, the plugin has
|
||||
// already been initialized
|
||||
if !effect.is_null() && (*effect).reserved1 != 0 {
|
||||
let reserved = (*effect).reserved1 as *const Arc<Mutex<T>>;
|
||||
let host = &*reserved;
|
||||
|
||||
let host = &mut *host.lock().unwrap();
|
||||
|
||||
interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt)
|
||||
// In this case, the plugin is still undergoing initialization and so `LOAD_POINTER` is
|
||||
// dereferenced
|
||||
} else {
|
||||
// Used only during the plugin initialization
|
||||
let host = LOAD_POINTER as *const Arc<Mutex<T>>;
|
||||
let host = &*host;
|
||||
let host = &mut *host.lock().unwrap();
|
||||
|
||||
interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::host::HostBuffer;
|
||||
|
||||
#[test]
|
||||
fn host_buffer() {
|
||||
const LENGTH: usize = 1_000_000;
|
||||
let mut host_buffer: HostBuffer<f32> = HostBuffer::new(2, 2);
|
||||
let input_left = vec![1.0; LENGTH];
|
||||
let input_right = vec![1.0; LENGTH];
|
||||
let mut output_left = vec![0.0; LENGTH];
|
||||
let mut output_right = vec![0.0; LENGTH];
|
||||
{
|
||||
let mut audio_buffer = {
|
||||
// Slices given to `bind` need not persist, but the sample arrays do.
|
||||
let inputs = [&input_left, &input_right];
|
||||
let mut outputs = [&mut output_left, &mut output_right];
|
||||
host_buffer.bind(&inputs, &mut outputs)
|
||||
};
|
||||
for (input, output) in audio_buffer.zip() {
|
||||
for (i, o) in input.iter().zip(output) {
|
||||
*o = *i * 2.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(output_left, vec![2.0; LENGTH]);
|
||||
assert_eq!(output_right, vec![2.0; LENGTH]);
|
||||
}
|
||||
}
|
370
plugin/vst/src/interfaces.rs
Normal file
370
plugin/vst/src/interfaces.rs
Normal file
|
@ -0,0 +1,370 @@
|
|||
//! Function interfaces for VST 2.4 API.
|
||||
|
||||
#![doc(hidden)]
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::os::raw::{c_char, c_void};
|
||||
use std::{mem, slice};
|
||||
|
||||
use crate::{
|
||||
api::{self, consts::*, AEffect, TimeInfo},
|
||||
buffer::AudioBuffer,
|
||||
editor::{Key, KeyCode, KnobMode, Rect},
|
||||
host::Host,
|
||||
};
|
||||
|
||||
/// Deprecated process function.
|
||||
pub extern "C" fn process_deprecated(
|
||||
_effect: *mut AEffect,
|
||||
_raw_inputs: *const *const f32,
|
||||
_raw_outputs: *mut *mut f32,
|
||||
_samples: i32,
|
||||
) {
|
||||
}
|
||||
|
||||
/// VST2.4 replacing function.
|
||||
pub extern "C" fn process_replacing(
|
||||
effect: *mut AEffect,
|
||||
raw_inputs: *const *const f32,
|
||||
raw_outputs: *mut *mut f32,
|
||||
samples: i32,
|
||||
) {
|
||||
// Handle to the VST
|
||||
let plugin = unsafe { (*effect).get_plugin() };
|
||||
let info = unsafe { (*effect).get_info() };
|
||||
let (input_count, output_count) = (info.inputs as usize, info.outputs as usize);
|
||||
let mut buffer =
|
||||
unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) };
|
||||
plugin.process(&mut buffer);
|
||||
}
|
||||
|
||||
/// VST2.4 replacing function with `f64` values.
|
||||
pub extern "C" fn process_replacing_f64(
|
||||
effect: *mut AEffect,
|
||||
raw_inputs: *const *const f64,
|
||||
raw_outputs: *mut *mut f64,
|
||||
samples: i32,
|
||||
) {
|
||||
let plugin = unsafe { (*effect).get_plugin() };
|
||||
let info = unsafe { (*effect).get_info() };
|
||||
let (input_count, output_count) = (info.inputs as usize, info.outputs as usize);
|
||||
let mut buffer =
|
||||
unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) };
|
||||
plugin.process_f64(&mut buffer);
|
||||
}
|
||||
|
||||
/// VST2.4 set parameter function.
|
||||
pub extern "C" fn set_parameter(effect: *mut AEffect, index: i32, value: f32) {
|
||||
unsafe { (*effect).get_params() }.set_parameter(index, value);
|
||||
}
|
||||
|
||||
/// VST2.4 get parameter function.
|
||||
pub extern "C" fn get_parameter(effect: *mut AEffect, index: i32) -> f32 {
|
||||
unsafe { (*effect).get_params() }.get_parameter(index)
|
||||
}
|
||||
|
||||
/// Copy a string into a destination buffer.
|
||||
///
|
||||
/// String will be cut at `max` characters.
|
||||
fn copy_string(dst: *mut c_void, src: &str, max: usize) -> isize {
|
||||
unsafe {
|
||||
use libc::{memcpy, memset};
|
||||
use std::cmp::min;
|
||||
|
||||
let dst = dst as *mut c_void;
|
||||
memset(dst, 0, max);
|
||||
memcpy(dst, src.as_ptr() as *const c_void, min(max, src.as_bytes().len()));
|
||||
}
|
||||
|
||||
1 // Success
|
||||
}
|
||||
|
||||
/// VST2.4 dispatch function. This function handles dispatching all opcodes to the VST plugin.
|
||||
pub extern "C" fn dispatch(
|
||||
effect: *mut AEffect,
|
||||
opcode: i32,
|
||||
index: i32,
|
||||
value: isize,
|
||||
ptr: *mut c_void,
|
||||
opt: f32,
|
||||
) -> isize {
|
||||
use crate::plugin::{CanDo, OpCode};
|
||||
|
||||
// Convert passed in opcode to enum
|
||||
let opcode = OpCode::try_from(opcode);
|
||||
// Only query plugin or editor when needed to avoid creating multiple
|
||||
// concurrent mutable references to the same object.
|
||||
let get_plugin = || unsafe { (*effect).get_plugin() };
|
||||
let get_editor = || unsafe { (*effect).get_editor() };
|
||||
let params = unsafe { (*effect).get_params() };
|
||||
|
||||
match opcode {
|
||||
Ok(OpCode::Initialize) => get_plugin().init(),
|
||||
Ok(OpCode::Shutdown) => unsafe {
|
||||
(*effect).drop_plugin();
|
||||
drop(Box::from_raw(effect))
|
||||
},
|
||||
|
||||
Ok(OpCode::ChangePreset) => params.change_preset(value as i32),
|
||||
Ok(OpCode::GetCurrentPresetNum) => return params.get_preset_num() as isize,
|
||||
Ok(OpCode::SetCurrentPresetName) => params.set_preset_name(read_string(ptr)),
|
||||
Ok(OpCode::GetCurrentPresetName) => {
|
||||
let num = params.get_preset_num();
|
||||
return copy_string(ptr, ¶ms.get_preset_name(num), MAX_PRESET_NAME_LEN);
|
||||
}
|
||||
|
||||
Ok(OpCode::GetParameterLabel) => {
|
||||
return copy_string(ptr, ¶ms.get_parameter_label(index), MAX_PARAM_STR_LEN)
|
||||
}
|
||||
Ok(OpCode::GetParameterDisplay) => {
|
||||
return copy_string(ptr, ¶ms.get_parameter_text(index), MAX_PARAM_STR_LEN)
|
||||
}
|
||||
Ok(OpCode::GetParameterName) => return copy_string(ptr, ¶ms.get_parameter_name(index), MAX_PARAM_STR_LEN),
|
||||
|
||||
Ok(OpCode::SetSampleRate) => get_plugin().set_sample_rate(opt),
|
||||
Ok(OpCode::SetBlockSize) => get_plugin().set_block_size(value as i64),
|
||||
Ok(OpCode::StateChanged) => {
|
||||
if value == 1 {
|
||||
get_plugin().resume();
|
||||
} else {
|
||||
get_plugin().suspend();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpCode::EditorGetRect) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
let size = editor.size();
|
||||
let pos = editor.position();
|
||||
|
||||
unsafe {
|
||||
// Given a Rect** structure
|
||||
// TODO: Investigate whether we are given a valid Rect** pointer already
|
||||
*(ptr as *mut *mut c_void) = Box::into_raw(Box::new(Rect {
|
||||
left: pos.0 as i16, // x coord of position
|
||||
top: pos.1 as i16, // y coord of position
|
||||
right: (pos.0 + size.0) as i16, // x coord of pos + x coord of size
|
||||
bottom: (pos.1 + size.1) as i16, // y coord of pos + y coord of size
|
||||
})) as *mut _; // TODO: free memory
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Ok(OpCode::EditorOpen) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
// `ptr` is a window handle to the parent window.
|
||||
// See the documentation for `Editor::open` for details.
|
||||
if editor.open(ptr) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(OpCode::EditorClose) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
editor.close();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpCode::EditorIdle) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
editor.idle();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpCode::GetData) => {
|
||||
let mut chunks = if index == 0 {
|
||||
params.get_bank_data()
|
||||
} else {
|
||||
params.get_preset_data()
|
||||
};
|
||||
|
||||
chunks.shrink_to_fit();
|
||||
let len = chunks.len() as isize; // eventually we should be using ffi::size_t
|
||||
|
||||
unsafe {
|
||||
*(ptr as *mut *mut c_void) = chunks.as_ptr() as *mut c_void;
|
||||
}
|
||||
|
||||
mem::forget(chunks);
|
||||
return len;
|
||||
}
|
||||
Ok(OpCode::SetData) => {
|
||||
let chunks = unsafe { slice::from_raw_parts(ptr as *mut u8, value as usize) };
|
||||
|
||||
if index == 0 {
|
||||
params.load_bank_data(chunks);
|
||||
} else {
|
||||
params.load_preset_data(chunks);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpCode::ProcessEvents) => {
|
||||
get_plugin().process_events(unsafe { &*(ptr as *const api::Events) });
|
||||
}
|
||||
Ok(OpCode::CanBeAutomated) => return params.can_be_automated(index) as isize,
|
||||
Ok(OpCode::StringToParameter) => return params.string_to_parameter(index, read_string(ptr)) as isize,
|
||||
|
||||
Ok(OpCode::GetPresetName) => return copy_string(ptr, ¶ms.get_preset_name(index), MAX_PRESET_NAME_LEN),
|
||||
|
||||
Ok(OpCode::GetInputInfo) => {
|
||||
if index >= 0 && index < get_plugin().get_info().inputs {
|
||||
unsafe {
|
||||
let ptr = ptr as *mut api::ChannelProperties;
|
||||
*ptr = get_plugin().get_input_info(index).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(OpCode::GetOutputInfo) => {
|
||||
if index >= 0 && index < get_plugin().get_info().outputs {
|
||||
unsafe {
|
||||
let ptr = ptr as *mut api::ChannelProperties;
|
||||
*ptr = get_plugin().get_output_info(index).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(OpCode::GetCategory) => {
|
||||
return get_plugin().get_info().category.into();
|
||||
}
|
||||
|
||||
Ok(OpCode::GetEffectName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_VENDOR_STR_LEN),
|
||||
|
||||
Ok(OpCode::GetVendorName) => return copy_string(ptr, &get_plugin().get_info().vendor, MAX_VENDOR_STR_LEN),
|
||||
Ok(OpCode::GetProductName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_PRODUCT_STR_LEN),
|
||||
Ok(OpCode::GetVendorVersion) => return get_plugin().get_info().version as isize,
|
||||
Ok(OpCode::VendorSpecific) => return get_plugin().vendor_specific(index, value, ptr, opt),
|
||||
Ok(OpCode::CanDo) => {
|
||||
let can_do = CanDo::from_str(&read_string(ptr));
|
||||
return get_plugin().can_do(can_do).into();
|
||||
}
|
||||
Ok(OpCode::GetTailSize) => {
|
||||
if get_plugin().get_tail_size() == 0 {
|
||||
return 1;
|
||||
} else {
|
||||
return get_plugin().get_tail_size();
|
||||
}
|
||||
}
|
||||
|
||||
//OpCode::GetParamInfo => { /*TODO*/ }
|
||||
Ok(OpCode::GetApiVersion) => return 2400,
|
||||
|
||||
Ok(OpCode::EditorKeyDown) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
if let Ok(key) = Key::try_from(value) {
|
||||
editor.key_down(KeyCode {
|
||||
character: index as u8 as char,
|
||||
key,
|
||||
modifier: opt.to_bits() as u8,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(OpCode::EditorKeyUp) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
if let Ok(key) = Key::try_from(value) {
|
||||
editor.key_up(KeyCode {
|
||||
character: index as u8 as char,
|
||||
key,
|
||||
modifier: opt.to_bits() as u8,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(OpCode::EditorSetKnobMode) => {
|
||||
if let Some(ref mut editor) = get_editor() {
|
||||
if let Ok(knob_mode) = KnobMode::try_from(value) {
|
||||
editor.set_knob_mode(knob_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OpCode::StartProcess) => get_plugin().start_process(),
|
||||
Ok(OpCode::StopProcess) => get_plugin().stop_process(),
|
||||
|
||||
Ok(OpCode::GetNumMidiInputs) => return unsafe { (*effect).get_info() }.midi_inputs as isize,
|
||||
Ok(OpCode::GetNumMidiOutputs) => return unsafe { (*effect).get_info() }.midi_outputs as isize,
|
||||
|
||||
_ => {
|
||||
debug!("Unimplemented opcode ({:?})", opcode);
|
||||
trace!(
|
||||
"Arguments; index: {}, value: {}, ptr: {:?}, opt: {}",
|
||||
index,
|
||||
value,
|
||||
ptr,
|
||||
opt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
pub fn host_dispatch(
|
||||
host: &mut dyn Host,
|
||||
effect: *mut AEffect,
|
||||
opcode: i32,
|
||||
index: i32,
|
||||
value: isize,
|
||||
ptr: *mut c_void,
|
||||
opt: f32,
|
||||
) -> isize {
|
||||
use crate::host::OpCode;
|
||||
|
||||
let opcode = OpCode::try_from(opcode);
|
||||
match opcode {
|
||||
Ok(OpCode::Version) => return 2400,
|
||||
Ok(OpCode::Automate) => host.automate(index, opt),
|
||||
Ok(OpCode::BeginEdit) => host.begin_edit(index),
|
||||
Ok(OpCode::EndEdit) => host.end_edit(index),
|
||||
|
||||
Ok(OpCode::Idle) => host.idle(),
|
||||
|
||||
// ...
|
||||
Ok(OpCode::CanDo) => {
|
||||
info!("Plugin is asking if host can: {}.", read_string(ptr));
|
||||
}
|
||||
|
||||
Ok(OpCode::GetVendorVersion) => return host.get_info().0,
|
||||
Ok(OpCode::GetVendorString) => return copy_string(ptr, &host.get_info().1, MAX_VENDOR_STR_LEN),
|
||||
Ok(OpCode::GetProductString) => return copy_string(ptr, &host.get_info().2, MAX_PRODUCT_STR_LEN),
|
||||
Ok(OpCode::ProcessEvents) => {
|
||||
host.process_events(unsafe { &*(ptr as *const api::Events) });
|
||||
}
|
||||
|
||||
Ok(OpCode::GetTime) => {
|
||||
return match host.get_time_info(value as i32) {
|
||||
None => 0,
|
||||
Some(result) => {
|
||||
thread_local! {
|
||||
static TIME_INFO: Cell<TimeInfo> =
|
||||
Cell::new(TimeInfo::default());
|
||||
}
|
||||
TIME_INFO.with(|time_info| {
|
||||
(*time_info).set(result);
|
||||
time_info.as_ptr() as isize
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(OpCode::GetBlockSize) => return host.get_block_size(),
|
||||
|
||||
_ => {
|
||||
trace!("VST: Got unimplemented host opcode ({:?})", opcode);
|
||||
trace!(
|
||||
"Arguments; effect: {:?}, index: {}, value: {}, ptr: {:?}, opt: {}",
|
||||
effect,
|
||||
index,
|
||||
value,
|
||||
ptr,
|
||||
opt
|
||||
);
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
// Read a string from the `ptr` buffer
|
||||
fn read_string(ptr: *mut c_void) -> String {
|
||||
use std::ffi::CStr;
|
||||
|
||||
String::from_utf8_lossy(unsafe { CStr::from_ptr(ptr as *mut c_char).to_bytes() }).into_owned()
|
||||
}
|
416
plugin/vst/src/lib.rs
Executable file
416
plugin/vst/src/lib.rs
Executable file
|
@ -0,0 +1,416 @@
|
|||
#![warn(missing_docs)]
|
||||
|
||||
//! A rust implementation of the VST2.4 API.
|
||||
//!
|
||||
//! The VST API is multi-threaded. A VST host calls into a plugin generally from two threads -
|
||||
//! the *processing* thread and the *UI* thread. The organization of this crate reflects this
|
||||
//! structure to ensure that the threading assumptions of Safe Rust are fulfilled and data
|
||||
//! races are avoided.
|
||||
//!
|
||||
//! # Plugins
|
||||
//! All Plugins must implement the `Plugin` trait and `std::default::Default`.
|
||||
//! The `plugin_main!` macro must also be called in order to export the necessary functions
|
||||
//! for the plugin to function.
|
||||
//!
|
||||
//! ## `Plugin` Trait
|
||||
//! All methods in this trait have a default implementation except for the `get_info` method which
|
||||
//! must be implemented by the plugin. Any of the default implementations may be overridden for
|
||||
//! custom functionality; the defaults do nothing on their own.
|
||||
//!
|
||||
//! ## `PluginParameters` Trait
|
||||
//! The methods in this trait handle access to plugin parameters. Since the host may call these
|
||||
//! methods concurrently with audio processing, it needs to be separate from the main `Plugin`
|
||||
//! trait.
|
||||
//!
|
||||
//! To support parameters, a plugin must provide an implementation of the `PluginParameters`
|
||||
//! trait, wrap it in an `Arc` (so it can be accessed from both threads) and
|
||||
//! return a reference to it from the `get_parameter_object` method in the `Plugin`.
|
||||
//!
|
||||
//! ## `plugin_main!` macro
|
||||
//! `plugin_main!` will export the necessary functions to create a proper VST plugin. This must be
|
||||
//! called with your VST plugin struct name in order for the vst to work.
|
||||
//!
|
||||
//! ## Example plugin
|
||||
//! A barebones VST plugin:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! #[macro_use]
|
||||
//! extern crate vst;
|
||||
//!
|
||||
//! use vst::plugin::{HostCallback, Info, Plugin};
|
||||
//!
|
||||
//! struct BasicPlugin;
|
||||
//!
|
||||
//! impl Plugin for BasicPlugin {
|
||||
//! fn new(_host: HostCallback) -> Self {
|
||||
//! BasicPlugin
|
||||
//! }
|
||||
//!
|
||||
//! fn get_info(&self) -> Info {
|
||||
//! Info {
|
||||
//! name: "Basic Plugin".to_string(),
|
||||
//! unique_id: 1357, // Used by hosts to differentiate between plugins.
|
||||
//!
|
||||
//! ..Default::default()
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! plugin_main!(BasicPlugin); // Important!
|
||||
//! # fn main() {} // For `extern crate vst`
|
||||
//! ```
|
||||
//!
|
||||
//! # Hosts
|
||||
//!
|
||||
//! ## `Host` Trait
|
||||
//! All hosts must implement the [`Host` trait](host/trait.Host.html). To load a VST plugin, you
|
||||
//! need to wrap your host in an `Arc<Mutex<T>>` wrapper for thread safety reasons. Along with the
|
||||
//! plugin path, this can be passed to the [`PluginLoader::load`] method to create a plugin loader
|
||||
//! which can spawn plugin instances.
|
||||
//!
|
||||
//! ## Example Host
|
||||
//! ```no_run
|
||||
//! extern crate vst;
|
||||
//!
|
||||
//! use std::sync::{Arc, Mutex};
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! use vst::host::{Host, PluginLoader};
|
||||
//! use vst::plugin::Plugin;
|
||||
//!
|
||||
//! struct SampleHost;
|
||||
//!
|
||||
//! impl Host for SampleHost {
|
||||
//! fn automate(&self, index: i32, value: f32) {
|
||||
//! println!("Parameter {} had its value changed to {}", index, value);
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let host = Arc::new(Mutex::new(SampleHost));
|
||||
//! let path = Path::new("/path/to/vst");
|
||||
//!
|
||||
//! let mut loader = PluginLoader::load(path, host.clone()).unwrap();
|
||||
//! let mut instance = loader.instance().unwrap();
|
||||
//!
|
||||
//! println!("Loaded {}", instance.get_info().name);
|
||||
//!
|
||||
//! instance.init();
|
||||
//! println!("Initialized instance!");
|
||||
//!
|
||||
//! println!("Closing instance...");
|
||||
//! // Not necessary as the instance is shut down when it goes out of scope anyway.
|
||||
//! // drop(instance);
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! [`PluginLoader::load`]: host/struct.PluginLoader.html#method.load
|
||||
//!
|
||||
|
||||
extern crate libc;
|
||||
extern crate libloading;
|
||||
extern crate num_enum;
|
||||
extern crate num_traits;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
use std::ptr;
|
||||
|
||||
pub mod api;
|
||||
pub mod buffer;
|
||||
mod cache;
|
||||
pub mod channels;
|
||||
pub mod editor;
|
||||
pub mod event;
|
||||
pub mod host;
|
||||
mod interfaces;
|
||||
pub mod plugin;
|
||||
pub mod prelude;
|
||||
pub mod util;
|
||||
|
||||
use api::consts::VST_MAGIC;
|
||||
use api::{AEffect, HostCallbackProc};
|
||||
use cache::PluginCache;
|
||||
use plugin::{HostCallback, Plugin};
|
||||
|
||||
/// Exports the necessary symbols for the plugin to be used by a VST host.
|
||||
///
|
||||
/// This macro takes a type which must implement the `Plugin` trait.
|
||||
#[macro_export]
|
||||
macro_rules! plugin_main {
|
||||
($t:ty) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[no_mangle]
|
||||
pub extern "system" fn main_macho(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||
VSTPluginMain(callback)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub extern "system" fn MAIN(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||
VSTPluginMain(callback)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn VSTPluginMain(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||
$crate::main::<$t>(callback)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Initializes a VST plugin and returns a raw pointer to an AEffect struct.
|
||||
#[doc(hidden)]
|
||||
pub fn main<T: Plugin>(callback: HostCallbackProc) -> *mut AEffect {
|
||||
// Initialize as much of the AEffect as we can before creating the plugin.
|
||||
// In particular, initialize all the function pointers, since initializing
|
||||
// these to zero is undefined behavior.
|
||||
let boxed_effect = Box::new(AEffect {
|
||||
magic: VST_MAGIC,
|
||||
dispatcher: interfaces::dispatch, // fn pointer
|
||||
|
||||
_process: interfaces::process_deprecated, // fn pointer
|
||||
|
||||
setParameter: interfaces::set_parameter, // fn pointer
|
||||
getParameter: interfaces::get_parameter, // fn pointer
|
||||
|
||||
numPrograms: 0, // To be updated with plugin specific value.
|
||||
numParams: 0, // To be updated with plugin specific value.
|
||||
numInputs: 0, // To be updated with plugin specific value.
|
||||
numOutputs: 0, // To be updated with plugin specific value.
|
||||
|
||||
flags: 0, // To be updated with plugin specific value.
|
||||
|
||||
reserved1: 0,
|
||||
reserved2: 0,
|
||||
|
||||
initialDelay: 0, // To be updated with plugin specific value.
|
||||
|
||||
_realQualities: 0,
|
||||
_offQualities: 0,
|
||||
_ioRatio: 0.0,
|
||||
|
||||
object: ptr::null_mut(),
|
||||
user: ptr::null_mut(),
|
||||
|
||||
uniqueId: 0, // To be updated with plugin specific value.
|
||||
version: 0, // To be updated with plugin specific value.
|
||||
|
||||
processReplacing: interfaces::process_replacing, // fn pointer
|
||||
processReplacingF64: interfaces::process_replacing_f64, //fn pointer
|
||||
|
||||
future: [0u8; 56],
|
||||
});
|
||||
let raw_effect = Box::into_raw(boxed_effect);
|
||||
|
||||
let host = HostCallback::wrap(callback, raw_effect);
|
||||
if host.vst_version() == 0 {
|
||||
// TODO: Better criteria would probably be useful here...
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
trace!("Creating VST plugin instance...");
|
||||
let mut plugin = T::new(host);
|
||||
let info = plugin.get_info();
|
||||
let params = plugin.get_parameter_object();
|
||||
let editor = plugin.get_editor();
|
||||
|
||||
// Update AEffect in place
|
||||
let effect = unsafe { &mut *raw_effect };
|
||||
effect.numPrograms = info.presets;
|
||||
effect.numParams = info.parameters;
|
||||
effect.numInputs = info.inputs;
|
||||
effect.numOutputs = info.outputs;
|
||||
effect.flags = {
|
||||
use api::PluginFlags;
|
||||
|
||||
let mut flag = PluginFlags::CAN_REPLACING;
|
||||
|
||||
if info.f64_precision {
|
||||
flag |= PluginFlags::CAN_DOUBLE_REPLACING;
|
||||
}
|
||||
|
||||
if editor.is_some() {
|
||||
flag |= PluginFlags::HAS_EDITOR;
|
||||
}
|
||||
|
||||
if info.preset_chunks {
|
||||
flag |= PluginFlags::PROGRAM_CHUNKS;
|
||||
}
|
||||
|
||||
if let plugin::Category::Synth = info.category {
|
||||
flag |= PluginFlags::IS_SYNTH;
|
||||
}
|
||||
|
||||
if info.silent_when_stopped {
|
||||
flag |= PluginFlags::NO_SOUND_IN_STOP;
|
||||
}
|
||||
|
||||
flag.bits()
|
||||
};
|
||||
effect.initialDelay = info.initial_delay;
|
||||
effect.object = Box::into_raw(Box::new(Box::new(plugin) as Box<dyn Plugin>)) as *mut _;
|
||||
effect.user = Box::into_raw(Box::new(PluginCache::new(&info, params, editor))) as *mut _;
|
||||
effect.uniqueId = info.unique_id;
|
||||
effect.version = info.version;
|
||||
|
||||
effect
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ptr;
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
use crate::{
|
||||
api::{consts::VST_MAGIC, AEffect},
|
||||
interfaces,
|
||||
plugin::{HostCallback, Info, Plugin},
|
||||
};
|
||||
|
||||
struct TestPlugin;
|
||||
|
||||
impl Plugin for TestPlugin {
|
||||
fn new(_host: HostCallback) -> Self {
|
||||
TestPlugin
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Info {
|
||||
Info {
|
||||
name: "Test Plugin".to_string(),
|
||||
vendor: "overdrivenpotato".to_string(),
|
||||
|
||||
presets: 1,
|
||||
parameters: 1,
|
||||
|
||||
unique_id: 5678,
|
||||
version: 1234,
|
||||
|
||||
initial_delay: 123,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_main!(TestPlugin);
|
||||
|
||||
extern "C" fn pass_callback(
|
||||
_effect: *mut AEffect,
|
||||
_opcode: i32,
|
||||
_index: i32,
|
||||
_value: isize,
|
||||
_ptr: *mut c_void,
|
||||
_opt: f32,
|
||||
) -> isize {
|
||||
1
|
||||
}
|
||||
|
||||
extern "C" fn fail_callback(
|
||||
_effect: *mut AEffect,
|
||||
_opcode: i32,
|
||||
_index: i32,
|
||||
_value: isize,
|
||||
_ptr: *mut c_void,
|
||||
_opt: f32,
|
||||
) -> isize {
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn old_hosts() {
|
||||
assert_eq!(MAIN(fail_callback), ptr::null_mut());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn old_hosts() {
|
||||
assert_eq!(main_macho(fail_callback), ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_callback() {
|
||||
assert_eq!(VSTPluginMain(fail_callback), ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aeffect_created() {
|
||||
let aeffect = VSTPluginMain(pass_callback);
|
||||
assert!(!aeffect.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_drop() {
|
||||
static mut DROP_TEST: bool = false;
|
||||
|
||||
impl Drop for TestPlugin {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
DROP_TEST = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let aeffect = VSTPluginMain(pass_callback);
|
||||
assert!(!aeffect.is_null());
|
||||
|
||||
unsafe { (*aeffect).drop_plugin() };
|
||||
|
||||
// Assert that the VST is shut down and dropped.
|
||||
assert!(unsafe { DROP_TEST });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_no_drop() {
|
||||
let aeffect = VSTPluginMain(pass_callback);
|
||||
assert!(!aeffect.is_null());
|
||||
|
||||
// Make sure this doesn't crash.
|
||||
unsafe { (*aeffect).drop_plugin() };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_deref() {
|
||||
let aeffect = VSTPluginMain(pass_callback);
|
||||
assert!(!aeffect.is_null());
|
||||
|
||||
let plugin = unsafe { (*aeffect).get_plugin() };
|
||||
// Assert that deref works correctly.
|
||||
assert!(plugin.get_info().name == "Test Plugin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aeffect_params() {
|
||||
// Assert that 2 function pointers are equal.
|
||||
macro_rules! assert_fn_eq {
|
||||
($a:expr, $b:expr) => {
|
||||
assert_eq!($a as usize, $b as usize);
|
||||
};
|
||||
}
|
||||
|
||||
let aeffect = unsafe { &mut *VSTPluginMain(pass_callback) };
|
||||
|
||||
assert_eq!(aeffect.magic, VST_MAGIC);
|
||||
assert_fn_eq!(aeffect.dispatcher, interfaces::dispatch);
|
||||
assert_fn_eq!(aeffect._process, interfaces::process_deprecated);
|
||||
assert_fn_eq!(aeffect.setParameter, interfaces::set_parameter);
|
||||
assert_fn_eq!(aeffect.getParameter, interfaces::get_parameter);
|
||||
assert_eq!(aeffect.numPrograms, 1);
|
||||
assert_eq!(aeffect.numParams, 1);
|
||||
assert_eq!(aeffect.numInputs, 2);
|
||||
assert_eq!(aeffect.numOutputs, 2);
|
||||
assert_eq!(aeffect.reserved1, 0);
|
||||
assert_eq!(aeffect.reserved2, 0);
|
||||
assert_eq!(aeffect.initialDelay, 123);
|
||||
assert_eq!(aeffect.uniqueId, 5678);
|
||||
assert_eq!(aeffect.version, 1234);
|
||||
assert_fn_eq!(aeffect.processReplacing, interfaces::process_replacing);
|
||||
assert_fn_eq!(aeffect.processReplacingF64, interfaces::process_replacing_f64);
|
||||
}
|
||||
}
|
1086
plugin/vst/src/plugin.rs
Normal file
1086
plugin/vst/src/plugin.rs
Normal file
File diff suppressed because it is too large
Load diff
12
plugin/vst/src/prelude.rs
Normal file
12
plugin/vst/src/prelude.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//! A collection of commonly used items for implement a Plugin
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use crate::api::{Events, Supported};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::buffer::{AudioBuffer, SendEventBuffer};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::event::{Event, MidiEvent};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::plugin::{CanDo, Category, HostCallback, Info, Plugin, PluginParameters};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::util::{AtomicFloat, ParameterTransfer};
|
59
plugin/vst/src/util/atomic_float.rs
Normal file
59
plugin/vst/src/util/atomic_float.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// Simple atomic floating point variable with relaxed ordering.
|
||||
///
|
||||
/// Designed for the common case of sharing VST parameters between
|
||||
/// multiple threads when no synchronization or change notification
|
||||
/// is needed.
|
||||
pub struct AtomicFloat {
|
||||
atomic: AtomicU32,
|
||||
}
|
||||
|
||||
impl AtomicFloat {
|
||||
/// New atomic float with initial value `value`.
|
||||
pub fn new(value: f32) -> AtomicFloat {
|
||||
AtomicFloat {
|
||||
atomic: AtomicU32::new(value.to_bits()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current value of the atomic float.
|
||||
pub fn get(&self) -> f32 {
|
||||
f32::from_bits(self.atomic.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Set the value of the atomic float to `value`.
|
||||
pub fn set(&self, value: f32) {
|
||||
self.atomic.store(value.to_bits(), Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AtomicFloat {
|
||||
fn default() -> Self {
|
||||
AtomicFloat::new(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AtomicFloat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(&self.get(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AtomicFloat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.get(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for AtomicFloat {
|
||||
fn from(value: f32) -> Self {
|
||||
AtomicFloat::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AtomicFloat> for f32 {
|
||||
fn from(value: AtomicFloat) -> Self {
|
||||
value.get()
|
||||
}
|
||||
}
|
7
plugin/vst/src/util/mod.rs
Normal file
7
plugin/vst/src/util/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
//! Structures for easing the implementation of VST plugins.
|
||||
|
||||
mod atomic_float;
|
||||
mod parameter_transfer;
|
||||
|
||||
pub use self::atomic_float::AtomicFloat;
|
||||
pub use self::parameter_transfer::{ParameterTransfer, ParameterTransferIterator};
|
187
plugin/vst/src/util/parameter_transfer.rs
Normal file
187
plugin/vst/src/util/parameter_transfer.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use std::mem::size_of;
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
|
||||
const USIZE_BITS: usize = size_of::<usize>() * 8;
|
||||
|
||||
fn word_and_bit(index: usize) -> (usize, usize) {
|
||||
(index / USIZE_BITS, 1usize << (index & (USIZE_BITS - 1)))
|
||||
}
|
||||
|
||||
/// A set of parameters that can be shared between threads.
|
||||
///
|
||||
/// Supports efficient iteration over parameters that changed since last iteration.
|
||||
#[derive(Default)]
|
||||
pub struct ParameterTransfer {
|
||||
values: Vec<AtomicU32>,
|
||||
changed: Vec<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ParameterTransfer {
|
||||
/// Create a new parameter set with `parameter_count` parameters.
|
||||
pub fn new(parameter_count: usize) -> Self {
|
||||
let bit_words = (parameter_count + USIZE_BITS - 1) / USIZE_BITS;
|
||||
ParameterTransfer {
|
||||
values: (0..parameter_count).map(|_| AtomicU32::new(0)).collect(),
|
||||
changed: (0..bit_words).map(|_| AtomicUsize::new(0)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the value of the parameter with index `index` to `value` and mark
|
||||
/// it as changed.
|
||||
pub fn set_parameter(&self, index: usize, value: f32) {
|
||||
let (word, bit) = word_and_bit(index);
|
||||
self.values[index].store(value.to_bits(), Ordering::Relaxed);
|
||||
self.changed[word].fetch_or(bit, Ordering::AcqRel);
|
||||
}
|
||||
|
||||
/// Get the current value of the parameter with index `index`.
|
||||
pub fn get_parameter(&self, index: usize) -> f32 {
|
||||
f32::from_bits(self.values[index].load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Iterate over all parameters marked as changed. If `acquire` is `true`,
|
||||
/// mark all returned parameters as no longer changed.
|
||||
///
|
||||
/// The iterator returns a pair of `(index, value)` for each changed parameter.
|
||||
///
|
||||
/// When parameters have been changed on the current thread, the iterator is
|
||||
/// precise: it reports all changed parameters with the values they were last
|
||||
/// changed to.
|
||||
///
|
||||
/// When parameters are changed on a different thread, the iterator is
|
||||
/// conservative, in the sense that it is guaranteed to report changed
|
||||
/// parameters eventually, but if a parameter is changed multiple times in
|
||||
/// a short period of time, it may skip some of the changes (but never the
|
||||
/// last) and may report an extra, spurious change at the end.
|
||||
///
|
||||
/// The changed parameters are reported in increasing index order, and the same
|
||||
/// parameter is never reported more than once in the same iteration.
|
||||
pub fn iterate(&self, acquire: bool) -> ParameterTransferIterator {
|
||||
ParameterTransferIterator {
|
||||
pt: self,
|
||||
word: 0,
|
||||
bit: 1,
|
||||
acquire,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over changed parameters.
|
||||
/// Returned by [`iterate`](struct.ParameterTransfer.html#method.iterate).
|
||||
pub struct ParameterTransferIterator<'pt> {
|
||||
pt: &'pt ParameterTransfer,
|
||||
word: usize,
|
||||
bit: usize,
|
||||
acquire: bool,
|
||||
}
|
||||
|
||||
impl<'pt> Iterator for ParameterTransferIterator<'pt> {
|
||||
type Item = (usize, f32);
|
||||
|
||||
fn next(&mut self) -> Option<(usize, f32)> {
|
||||
let bits = loop {
|
||||
if self.word == self.pt.changed.len() {
|
||||
return None;
|
||||
}
|
||||
let bits = self.pt.changed[self.word].load(Ordering::Acquire) & self.bit.wrapping_neg();
|
||||
if bits != 0 {
|
||||
break bits;
|
||||
}
|
||||
self.word += 1;
|
||||
self.bit = 1;
|
||||
};
|
||||
|
||||
let bit_index = bits.trailing_zeros() as usize;
|
||||
let bit = 1usize << bit_index;
|
||||
let index = self.word * USIZE_BITS + bit_index;
|
||||
|
||||
if self.acquire {
|
||||
self.pt.changed[self.word].fetch_and(!bit, Ordering::AcqRel);
|
||||
}
|
||||
|
||||
let next_bit = bit << 1;
|
||||
if next_bit == 0 {
|
||||
self.word += 1;
|
||||
self.bit = 1;
|
||||
} else {
|
||||
self.bit = next_bit;
|
||||
}
|
||||
|
||||
Some((index, self.pt.get_parameter(index)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate rand;
|
||||
|
||||
use crate::util::ParameterTransfer;
|
||||
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use self::rand::rngs::StdRng;
|
||||
use self::rand::{Rng, SeedableRng};
|
||||
|
||||
const THREADS: usize = 3;
|
||||
const PARAMETERS: usize = 1000;
|
||||
const UPDATES: usize = 1_000_000;
|
||||
|
||||
#[test]
|
||||
fn parameter_transfer() {
|
||||
let transfer = Arc::new(ParameterTransfer::new(PARAMETERS));
|
||||
let (tx, rx) = channel();
|
||||
|
||||
// Launch threads that change parameters
|
||||
for t in 0..THREADS {
|
||||
let t_transfer = Arc::clone(&transfer);
|
||||
let t_tx = tx.clone();
|
||||
let mut t_rng = StdRng::seed_from_u64(t as u64);
|
||||
thread::spawn(move || {
|
||||
let mut values = vec![0f32; PARAMETERS];
|
||||
for _ in 0..UPDATES {
|
||||
let p: usize = t_rng.gen_range(0..PARAMETERS);
|
||||
let v: f32 = t_rng.gen_range(0.0..1.0);
|
||||
values[p] = v;
|
||||
t_transfer.set_parameter(p, v);
|
||||
}
|
||||
t_tx.send(values).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// Continually receive updates from threads
|
||||
let mut values = vec![0f32; PARAMETERS];
|
||||
let mut results = vec![];
|
||||
let mut acquire_rng = StdRng::seed_from_u64(42);
|
||||
while results.len() < THREADS {
|
||||
let mut last_p = -1;
|
||||
for (p, v) in transfer.iterate(acquire_rng.gen_bool(0.9)) {
|
||||
assert!(p as isize > last_p);
|
||||
last_p = p as isize;
|
||||
values[p] = v;
|
||||
}
|
||||
thread::sleep(Duration::from_micros(100));
|
||||
while let Ok(result) = rx.try_recv() {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// One last iteration to pick up all updates
|
||||
let mut last_p = -1;
|
||||
for (p, v) in transfer.iterate(true) {
|
||||
assert!(p as isize > last_p);
|
||||
last_p = p as isize;
|
||||
values[p] = v;
|
||||
}
|
||||
|
||||
// Now there should be no more updates
|
||||
assert!(transfer.iterate(true).next().is_none());
|
||||
|
||||
// Verify final values
|
||||
for p in 0..PARAMETERS {
|
||||
assert!((0..THREADS).any(|t| results[t][p] == values[p]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -163,7 +163,7 @@ impl Tek {
|
|||
midi_froms: &[PortConnect], midi_tos: &[PortConnect],
|
||||
audio_froms: &[&[PortConnect];2], audio_tos: &[&[PortConnect];2],
|
||||
) -> Usually<Self> {
|
||||
let app = Self {
|
||||
let tek = Self {
|
||||
view: SourceIter(include_str!("./view_groovebox.edn")),
|
||||
tracks: vec![Track {
|
||||
devices: vec![Sampler::new(jack, &"sampler", midi_froms, audio_froms, audio_tos)?.boxed()],
|
||||
|
@ -171,10 +171,10 @@ impl Tek {
|
|||
}],
|
||||
..Self::new_sequencer(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
|
||||
};
|
||||
//if let Some(sampler) = app.sampler.as_ref().unwrap().midi_in.as_ref() {
|
||||
//app.player.as_ref().unwrap().midi_outs[0].connect_to(sampler.port())?;
|
||||
//if let Some(sampler) = tek.sampler.as_ref().unwrap().midi_in.as_ref() {
|
||||
//tek.player.as_ref().unwrap().midi_outs[0].connect_to(sampler.port())?;
|
||||
//}
|
||||
Ok(app)
|
||||
Ok(tek)
|
||||
}
|
||||
pub fn new_arranger (
|
||||
jack: &Jack,
|
||||
|
@ -183,7 +183,7 @@ impl Tek {
|
|||
audio_froms: &[&[PortConnect];2], audio_tos: &[&[PortConnect];2],
|
||||
scenes: usize, tracks: usize, track_width: usize,
|
||||
) -> Usually<Self> {
|
||||
let mut arranger = Self {
|
||||
let mut tek = Self {
|
||||
view: SourceIter(include_str!("./view_arranger.edn")),
|
||||
pool: Some(Default::default()),
|
||||
editor: Some(Default::default()),
|
||||
|
@ -193,14 +193,12 @@ impl Tek {
|
|||
scenes: vec![],
|
||||
..Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
|
||||
};
|
||||
arranger.scenes_add(scenes);
|
||||
arranger.tracks_add(tracks, Some(track_width), &[], &[]);
|
||||
arranger.selected = Selection::Clip(1, 1);
|
||||
arranger.arranger = BigBuffer::new(
|
||||
arranger.w_tracks() as usize,
|
||||
arranger.h_scenes() as usize,
|
||||
);
|
||||
Ok(arranger)
|
||||
tek.scenes_add(scenes);
|
||||
tek.tracks_add(tracks, Some(track_width), &[], &[]);
|
||||
tek.selected = Selection::Clip(1, 1);
|
||||
tek.arranger = Default::default();
|
||||
tek.redraw_arranger();
|
||||
Ok(tek)
|
||||
}
|
||||
}
|
||||
#[cfg(test)] fn test_tek_cli () {
|
||||
|
|
|
@ -6,28 +6,26 @@
|
|||
#![feature(impl_trait_in_assoc_type)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(trait_alias)]
|
||||
mod cli; pub use self::cli::*;
|
||||
mod audio; pub use self::audio::*;
|
||||
|
||||
mod keys; pub use self::keys::*;
|
||||
mod keys_clip; pub use self::keys_clip::*;
|
||||
mod keys_ins; pub use self::keys_ins::*;
|
||||
mod keys_outs; pub use self::keys_outs::*;
|
||||
mod keys_scene; pub use self::keys_scene::*;
|
||||
mod keys_track; pub use self::keys_track::*;
|
||||
|
||||
mod model; pub use self::model::*;
|
||||
mod model_track; pub use self::model_track::*;
|
||||
mod model_scene; pub use self::model_scene::*;
|
||||
mod model_select; pub use self::model_select::*;
|
||||
|
||||
mod view; pub use self::view::*;
|
||||
mod view_memo; pub use self::view_memo::*;
|
||||
mod view_clock; pub use self::view_clock::*;
|
||||
mod view_clips; pub use self::view_clips::*;
|
||||
mod view_meter; pub use self::view_meter::*;
|
||||
mod view_input; pub use self::view_input::*;
|
||||
mod view_output; pub use self::view_output::*;
|
||||
mod cli; pub use self::cli::*;
|
||||
mod audio; pub use self::audio::*;
|
||||
mod keys; pub use self::keys::*;
|
||||
mod keys_clip; pub use self::keys_clip::*;
|
||||
mod keys_ins; pub use self::keys_ins::*;
|
||||
mod keys_outs; pub use self::keys_outs::*;
|
||||
mod keys_scene; pub use self::keys_scene::*;
|
||||
mod keys_track; pub use self::keys_track::*;
|
||||
mod model; pub use self::model::*;
|
||||
mod model_track; pub use self::model_track::*;
|
||||
mod model_scene; pub use self::model_scene::*;
|
||||
mod model_select; pub use self::model_select::*;
|
||||
mod view; pub use self::view::*;
|
||||
mod view_arranger; pub use self::view_arranger::*;
|
||||
mod view_clock; pub use self::view_clock::*;
|
||||
mod view_color; pub use self::view_color::*;
|
||||
mod view_iter; pub use self::view_iter::*;
|
||||
mod view_memo; pub use self::view_memo::*;
|
||||
mod view_meter; pub use self::view_meter::*;
|
||||
mod view_sizes; pub use self::view_sizes::*;
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
/// Standard optional result type.
|
||||
|
|
|
@ -10,8 +10,8 @@ use crate::*;
|
|||
pub pool: Option<MidiPool>,
|
||||
/// Contains the currently edited MIDI clip
|
||||
pub editor: Option<MidiEditor>,
|
||||
/// Contains the project arrangement
|
||||
pub arranger: BigBuffer,
|
||||
/// Contains a render of the project arrangement, redrawn on update.
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
pub midi_outs: Vec<JackMidiOut>,
|
||||
pub audio_ins: Vec<JackAudioIn>,
|
||||
|
|
|
@ -104,37 +104,56 @@ impl Tek {
|
|||
content: impl Content<TuiOut> + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let count = format!("{count}");
|
||||
Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(self.button_3(key, label, count))), content)))
|
||||
Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, self.is_editing()))), content)))
|
||||
}
|
||||
pub(crate) fn button_2 <'a, K, L> (&'a self, key: K, label: L) -> impl Content<TuiOut> + 'a
|
||||
where K: Content<TuiOut> + 'a, L: Content<TuiOut> + 'a {
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(),
|
||||
Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(Tui::g(96), "▐"))));
|
||||
Tui::bold(true, Bsp::e(key,
|
||||
When::new(!self.is_editing(), Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) }
|
||||
pub(crate) fn button_3 <'a, K, L, V> (&'a self, key: K, label: L, value: V) -> impl Content<TuiOut> + 'a
|
||||
where K: Content<TuiOut> + 'a, L: Content<TuiOut> + 'a, V: Content<TuiOut> + 'a {
|
||||
let editing = self.is_editing();
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(),
|
||||
Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing {
|
||||
Tui::g(128)
|
||||
} else {
|
||||
Tui::g(96)
|
||||
}, "▐"))));
|
||||
Tui::bold(true, Bsp::e(key, Bsp::e(
|
||||
When::new(!editing, Bsp::e(
|
||||
Tui::fg_bg(Tui::g(255), Tui::g(96), label),
|
||||
Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"),
|
||||
)),
|
||||
Bsp::e(
|
||||
Tui::fg_bg(Tui::g(224), Tui::g(128), value),
|
||||
Tui::fg_bg(Tui::g(128), Reset, "▌"),
|
||||
)
|
||||
))) }
|
||||
pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||
Bsp::e(Tui::fg_bg(bg, Reset, "▐"), Bsp::w(Tui::fg_bg(bg, Reset, "▌"),
|
||||
Tui::fg_bg(fg, bg, content))) }
|
||||
}
|
||||
|
||||
pub(crate) fn button_2 <'a, K, L> (
|
||||
key: K,
|
||||
label: L,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a where
|
||||
K: Content<TuiOut> + 'a,
|
||||
L: Content<TuiOut> + 'a,
|
||||
{
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e(
|
||||
Tui::fg_bg(Tui::orange(), Reset, "▐"),
|
||||
Bsp::e(key, Tui::fg(Tui::g(96), "▐"))
|
||||
));
|
||||
let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label));
|
||||
Tui::bold(true, Bsp::e(key, label))
|
||||
}
|
||||
|
||||
pub(crate) fn button_3 <'a, K, L, V> (
|
||||
key: K,
|
||||
label: L,
|
||||
value: V,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a where
|
||||
K: Content<TuiOut> + 'a,
|
||||
L: Content<TuiOut> + 'a,
|
||||
V: Content<TuiOut> + 'a,
|
||||
{
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(),
|
||||
Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing {
|
||||
Tui::g(128)
|
||||
} else {
|
||||
Tui::g(96)
|
||||
}, "▐"))));
|
||||
let label = Bsp::e(
|
||||
When::new(!editing, Bsp::e(
|
||||
Tui::fg_bg(Tui::g(255), Tui::g(96), label),
|
||||
Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"),
|
||||
)),
|
||||
Bsp::e(
|
||||
Tui::fg_bg(Tui::g(224), Tui::g(128), value),
|
||||
Tui::fg_bg(Tui::g(128), Reset, "▌"),
|
||||
));
|
||||
Tui::bold(true, Bsp::e(key, label))
|
||||
}
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
#[test] fn test_view () {
|
||||
|
@ -150,8 +169,10 @@ impl Tek {
|
|||
let _ = app.row_top(0, 0, "", "", "");
|
||||
//let _ = app.io_ports(Reset, Reset, ||[].iter());
|
||||
//let _ = app.io_connections(Reset, Reset, ||[].iter());
|
||||
let _ = app.button_2("", "");
|
||||
let _ = app.button_3("", "", "");
|
||||
let _ = app.button_2("", "", true);
|
||||
let _ = app.button_2("", "", false);
|
||||
let _ = app.button_3("", "", "", true);
|
||||
let _ = app.button_3("", "", "", false);
|
||||
let _ = app.heading("", "", 0, "");
|
||||
let _ = Tek::wrap(Reset, Reset, "");
|
||||
}
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
(bsp/n (fixed/y 1 :transport)
|
||||
(bsp/s (fixed/y 1 :status)
|
||||
(fill/xy (bsp/a (fill/xy (align/e :pool))
|
||||
(bsp/s :inputs (bsp/s :tracks (bsp/n :outputs :scenes)))))))
|
||||
(bsp/n (fixed/y 1 :transport) (bsp/s (fixed/y 1 :status) (fill/xy (bsp/a (fill/xy (align/e :pool)) :arranger))))
|
||||
|
|
|
@ -1,91 +1,45 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
const TAB: &str = " Tab";
|
||||
const TRACK_SPACING: usize = 0;
|
||||
const H_SCENE: usize = 2;
|
||||
const H_EDITOR: usize = 15;
|
||||
|
||||
pub(crate) fn w_tracks_area (&self) -> u16 {
|
||||
self.w().saturating_sub(2 * self.w_sidebar())
|
||||
/// Blit the currently visible section of the arranger to the output.
|
||||
///
|
||||
/// If the arranger is larger than the available display area,
|
||||
/// the scrollbars determine the portion that will be shown.
|
||||
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
()
|
||||
}
|
||||
pub(crate) fn w_tracks (&self) -> u16 {
|
||||
self.tracks_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
|
||||
}
|
||||
fn track_scrollbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::x(Fixed::y(1, ScrollbarH {
|
||||
offset: self.track_scroll,
|
||||
length: self.w_tracks_area() as usize,
|
||||
total: self.w_tracks() as usize,
|
||||
}))
|
||||
}
|
||||
pub(crate) fn h_tracks_area (&self) -> u16 {
|
||||
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 10)
|
||||
}
|
||||
pub(crate) fn h_scenes (&self) -> u16 {
|
||||
self.scenes_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last().map(|(_, _, _, y)|y as u16).unwrap_or(0)
|
||||
/// Draw the full arranger to the arranger view buffer.
|
||||
///
|
||||
/// This should happen on changes to the arrangement view
|
||||
/// other than scrolling. Scrolling should just determine
|
||||
/// which part of the arranger buffer to blit to output.
|
||||
pub fn redraw_arranger (&self) {
|
||||
let width = self.w_tracks();
|
||||
let height = self.h_scenes() + self.h_inputs() + self.h_outputs();
|
||||
let buffer = Buffer::empty(ratatui::prelude::Rect { x: 0, y: 0, width, height });
|
||||
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
|
||||
let content = Bsp::s(self.view_inputs(),
|
||||
Bsp::s(self.view_tracks(),
|
||||
Bsp::n(self.view_outputs(), self.view_scenes())));
|
||||
Content::render(&content, &mut output);
|
||||
*self.arranger.write().unwrap() = output.buffer;
|
||||
}
|
||||
/// Display the current scene scroll state.
|
||||
fn scene_scrollbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::y(Fixed::x(1, ScrollbarV {
|
||||
offset: self.scene_scroll,
|
||||
length: self.h_tracks_area() as usize,
|
||||
total: self.h_scenes() as usize,
|
||||
total: self.h_scenes() as usize,
|
||||
}))
|
||||
}
|
||||
fn tracks_sizes <'a> (&'a self) -> impl TracksSizes<'a> {
|
||||
let editing = self.is_editing();
|
||||
let bigger = self.editor_w();
|
||||
let mut x = 0;
|
||||
let active = match self.selected() {
|
||||
Selection::Track(t) if editing => Some(t),
|
||||
Selection::Clip(t, _) if editing => Some(t),
|
||||
_ => None
|
||||
};
|
||||
self.tracks().iter().enumerate().map(move |(index, track)|{
|
||||
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
data
|
||||
})
|
||||
}
|
||||
fn scenes_sizes (&self, editing: bool, height: usize, larger: usize) -> impl ScenesSizes<'_> {
|
||||
let (selected_track, selected_scene) = match self.selected() {
|
||||
Selection::Track(t) => (Some(*t), None),
|
||||
Selection::Scene(s) => (None, Some(*s)),
|
||||
Selection::Clip(t, s) => (Some(*t), Some(*s)),
|
||||
_ => (None, None)
|
||||
};
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().map(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
fn scenes_with_colors (&self, editing: bool, h: u16) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].color)
|
||||
}))
|
||||
})
|
||||
}
|
||||
fn scenes_with_track_colors (&self, editing: bool, h: u16, t: usize) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].clips[t].as_ref()
|
||||
.map(|c|c.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::G[32]))
|
||||
}))
|
||||
})
|
||||
/// Display the current track scroll state.
|
||||
fn track_scrollbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::x(Fixed::y(1, ScrollbarH {
|
||||
offset: self.track_scroll,
|
||||
length: self.w_tracks_area() as usize,
|
||||
total: self.w_tracks() as usize,
|
||||
}))
|
||||
}
|
||||
|
||||
fn per_track <'a, T: Content<TuiOut> + 'a> (
|
||||
|
@ -93,8 +47,7 @@ impl Tek {
|
|||
) -> impl Content<TuiOut> + 'a {
|
||||
self.per_track_top(move|index, track|Fill::y(Align::y(f(index, track))))
|
||||
}
|
||||
|
||||
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a> (
|
||||
fn per_track_top <'a, T: Content<TuiOut> + 'a> (
|
||||
&'a self, f: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let width = self.w_tracks_area();
|
||||
|
@ -112,10 +65,11 @@ impl Tek {
|
|||
pub fn view_tracks (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = (self.size.w() as u16).saturating_sub(2 * self.w_sidebar());
|
||||
let data = (self.selected.track().unwrap_or(0), self.tracks().len());
|
||||
let editing = self.is_editing();
|
||||
self.fmtd.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1));
|
||||
self.row(w, 1, self.button_3("t", "track", self.fmtd.read().unwrap().trks.view.clone()),
|
||||
self.row(w, 1, button_3("t", "track", self.fmtd.read().unwrap().trks.view.clone(), editing),
|
||||
self.per_track(|t, track|self.view_track_header(t, track)),
|
||||
self.button_2("T", "add track"))
|
||||
button_2("T", "add track", editing))
|
||||
}
|
||||
|
||||
fn view_track_header <'a> (&self, t: usize, track: &'a Track) -> impl Content<TuiOut> + use<'a> {
|
||||
|
@ -162,9 +116,17 @@ impl Tek {
|
|||
}
|
||||
|
||||
fn view_scene_clip (
|
||||
&self, width: u16, height: u16, offset: u16,
|
||||
scene: &Scene, prev: Option<ItemPalette>, s: usize, t: usize,
|
||||
editing: bool, same_track: bool, selected_scene: Option<usize>
|
||||
&self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
offset: u16,
|
||||
scene: &Scene,
|
||||
prev: Option<ItemPalette>,
|
||||
s: usize,
|
||||
t: usize,
|
||||
editing: bool,
|
||||
same_track: bool,
|
||||
selected_scene: Option<usize>
|
||||
) -> impl Content<TuiOut> + use<'_> {
|
||||
let (name, fg, bg) = if let Some(clip) = &scene.clips[t] {
|
||||
let clip = clip.read().unwrap();
|
||||
|
@ -201,23 +163,82 @@ impl Tek {
|
|||
}
|
||||
|
||||
pub fn view_scene_add (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let data = (self.selected().scene().unwrap_or(0), self.scenes().len());
|
||||
self.fmtd.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1));
|
||||
self.button_3("S", "add scene", self.fmtd.read().unwrap().scns.view.clone())
|
||||
button_3("S", "add scene", self.fmtd.read().unwrap().scns.view.clone(), editing)
|
||||
}
|
||||
pub fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let w = self.w_tracks_area();
|
||||
let fg = Tui::g(224);
|
||||
let nexts = self.per_track_top(|t, track|Either(
|
||||
track.player.next_clip.is_some(),
|
||||
Thunk::new(||Tui::bg(Reset, format!("{:?}",
|
||||
track.player.next_clip.as_ref()
|
||||
.map(|(moment, clip)|clip.as_ref()
|
||||
.map(|clip|clip.read().unwrap().name.clone()))
|
||||
.flatten().as_ref()))),
|
||||
Thunk::new(||Tui::bg(Reset, " ------ "))));
|
||||
let nexts = self.row_top(w, 2, Align::ne("Next:"), nexts, ());
|
||||
let froms = self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default(),))));
|
||||
let froms = self.row_top(w, 2, Align::ne("From:"), froms, ());
|
||||
let ports = self.row_top(w, 1,
|
||||
button_3("o", "midi outs", format!("{}", self.midi_outs.len()), editing),
|
||||
self.per_track_top(move|t, track|{
|
||||
let mute = false;
|
||||
let solo = false;
|
||||
let mute = if mute { White } else { track.color.darkest.rgb };
|
||||
let solo = if solo { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) { track.color.light.rgb } else { track.color.base.rgb };
|
||||
let bg2 = if t > 0 { self.tracks()[t].color.base.rgb } else { Reset };
|
||||
Self::wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(mute, bg, "Play "),
|
||||
Tui::fg_bg(solo, bg, "Solo ")))))}),
|
||||
button_2("O", "add midi out", editing));
|
||||
let routes = self.row_top(w, self.h_outputs() - 1,
|
||||
self.io_ports(fg, Tui::g(32), ||self.outputs_sizes()),
|
||||
self.per_track_top(move|t, track|self.io_connections(
|
||||
track.color.dark.rgb, track.color.darker.rgb, ||self.outputs_sizes())), ());
|
||||
|
||||
Align::n(Bsp::s(Bsp::s(nexts, froms), Bsp::s(ports, routes)))
|
||||
}
|
||||
pub fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let w = (self.size.w() as u16).saturating_sub(2 * self.w_sidebar());
|
||||
let fg = Tui::g(224);
|
||||
let routes = self.row_top(w, self.h_inputs() - 1,
|
||||
self.io_ports(fg, Tui::g(32), ||self.inputs_sizes()),
|
||||
self.per_track_top(move|t, track|self.io_connections(
|
||||
track.color.dark.rgb,
|
||||
track.color.darker.rgb,
|
||||
||self.inputs_sizes()
|
||||
)), ());
|
||||
let ports = self.row_top(w, 1,
|
||||
button_3("i", "midi ins", format!("{}", self.midi_ins.len()), editing),
|
||||
self.per_track_top(move|t, track|{
|
||||
let rec = track.player.recording;
|
||||
let mon = track.player.monitoring;
|
||||
let rec = if rec { White } else { track.color.darkest.rgb };
|
||||
let mon = if mon { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
let bg2 = if t > 0 { self.tracks()[t - 1].color.base.rgb } else { Reset };
|
||||
Self::wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(rec, bg, "Rec "),
|
||||
Tui::fg_bg(mon, bg, "Mon ")))))
|
||||
}),
|
||||
button_2("I", "add midi in", editing));
|
||||
Bsp::s(
|
||||
Bsp::s(routes, ports),
|
||||
self.row_top(w, 2,
|
||||
Bsp::s(Align::e("Input:"), Align::e("Into:")),
|
||||
self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(
|
||||
OctaveVertical::default(),
|
||||
" ------ ")))), ())
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Tek {
|
||||
fn colors (
|
||||
theme: &ItemPalette, prev: Option<ItemPalette>,
|
||||
selected: bool, neighbor: bool, is_last: bool,
|
||||
) -> [Color;4] {
|
||||
let fg = theme.lightest.rgb;
|
||||
let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
let hi = Self::color_hi(prev, neighbor);
|
||||
let lo = Self::color_lo(theme, is_last, selected);
|
||||
[fg, bg, hi, lo] }
|
||||
fn color_hi (prev: Option<ItemPalette>, neighbor: bool) -> Color {
|
||||
prev.map(|prev|if neighbor { prev.light.rgb } else { prev.base.rgb }).unwrap_or(Reset) }
|
||||
fn color_lo (theme: &ItemPalette, is_last: bool, selected: bool) -> Color {
|
||||
if is_last { Reset } else if selected { theme.light.rgb } else { theme.base.rgb } }
|
||||
}
|
22
tek/src/view_color.rs
Normal file
22
tek/src/view_color.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub(crate) fn colors (
|
||||
theme: &ItemPalette,
|
||||
prev: Option<ItemPalette>,
|
||||
selected: bool,
|
||||
neighbor: bool,
|
||||
is_last: bool,
|
||||
) -> [Color;4] {
|
||||
let fg = theme.lightest.rgb;
|
||||
let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
let hi = Self::color_hi(prev, neighbor);
|
||||
let lo = Self::color_lo(theme, is_last, selected);
|
||||
[fg, bg, hi, lo]
|
||||
}
|
||||
pub(crate) fn color_hi (prev: Option<ItemPalette>, neighbor: bool) -> Color {
|
||||
prev.map(|prev|if neighbor { prev.light.rgb } else { prev.base.rgb }).unwrap_or(Reset)
|
||||
}
|
||||
pub(crate) fn color_lo (theme: &ItemPalette, is_last: bool, selected: bool) -> Color {
|
||||
if is_last { Reset } else if selected { theme.light.rgb } else { theme.base.rgb }
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = (self.size.w() as u16).saturating_sub(2 * self.w_sidebar());
|
||||
let fg = Tui::g(224);
|
||||
let routes = self.row_top(w, self.h_inputs() - 1,
|
||||
self.io_ports(fg, Tui::g(32), ||self.inputs_sizes()),
|
||||
self.per_track_top(move|t, track|self.io_connections(
|
||||
track.color.dark.rgb,
|
||||
track.color.darker.rgb,
|
||||
||self.inputs_sizes()
|
||||
)), ());
|
||||
let ports = self.row_top(w, 1,
|
||||
self.button_3("i", "midi ins", format!("{}", self.midi_ins.len())),
|
||||
self.per_track_top(move|t, track|{
|
||||
let rec = track.player.recording;
|
||||
let mon = track.player.monitoring;
|
||||
let rec = if rec { White } else { track.color.darkest.rgb };
|
||||
let mon = if mon { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
let bg2 = if t > 0 { self.tracks()[t - 1].color.base.rgb } else { Reset };
|
||||
Self::wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(rec, bg, "Rec "),
|
||||
Tui::fg_bg(mon, bg, "Mon ")))))
|
||||
}),
|
||||
self.button_2("I", "add midi in"));
|
||||
Bsp::s(
|
||||
Bsp::s(routes, ports),
|
||||
self.row_top(w, 2,
|
||||
Bsp::s(Align::e("Input:"), Align::e("Into:")),
|
||||
self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(
|
||||
OctaveVertical::default(),
|
||||
" ------ ")))), ())
|
||||
)
|
||||
}
|
||||
pub(crate) fn h_inputs (&self) -> u16 {
|
||||
1 + self.inputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
pub(crate) fn inputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_ins.iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.conn().len();
|
||||
let data = (i, input.name(), input.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
77
tek/src/view_iter.rs
Normal file
77
tek/src/view_iter.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub(crate) fn inputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_ins.iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.conn().len();
|
||||
let data = (i, input.name(), input.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn outputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_outs.iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.conn().len();
|
||||
let data = (i, output.name(), output.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn tracks_sizes <'a> (&'a self) -> impl TracksSizes<'a> {
|
||||
let editing = self.is_editing();
|
||||
let bigger = self.editor_w();
|
||||
let mut x = 0;
|
||||
let active = match self.selected() {
|
||||
Selection::Track(t) if editing => Some(t),
|
||||
Selection::Clip(t, _) if editing => Some(t),
|
||||
_ => None
|
||||
};
|
||||
self.tracks().iter().enumerate().map(move |(index, track)|{
|
||||
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_sizes (&self, editing: bool, height: usize, larger: usize) -> impl ScenesSizes<'_> {
|
||||
let (selected_track, selected_scene) = match self.selected() {
|
||||
Selection::Track(t) => (Some(*t), None),
|
||||
Selection::Scene(s) => (None, Some(*s)),
|
||||
Selection::Clip(t, s) => (Some(*t), Some(*s)),
|
||||
_ => (None, None)
|
||||
};
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().map(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_with_colors (&self, editing: bool, h: u16) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].color)
|
||||
}))
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_with_track_colors (&self, editing: bool, h: u16, t: usize) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].clips[t].as_ref()
|
||||
.map(|c|c.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::G[32]))
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = self.w_tracks_area();
|
||||
let fg = Tui::g(224);
|
||||
let nexts = self.per_track_top(|t, track|Either(
|
||||
track.player.next_clip.is_some(),
|
||||
Thunk::new(||Tui::bg(Reset, format!("{:?}",
|
||||
track.player.next_clip.as_ref()
|
||||
.map(|(moment, clip)|clip.as_ref()
|
||||
.map(|clip|clip.read().unwrap().name.clone()))
|
||||
.flatten().as_ref()))),
|
||||
Thunk::new(||Tui::bg(Reset, " ------ "))));
|
||||
let nexts = self.row_top(w, 2, Align::ne("Next:"), nexts, ());
|
||||
let froms = self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default(),))));
|
||||
let froms = self.row_top(w, 2, Align::ne("From:"), froms, ());
|
||||
let ports = self.row_top(w, 1,
|
||||
self.button_3("o", "midi outs", format!("{}", self.midi_outs.len())),
|
||||
self.per_track_top(move|t, track|{
|
||||
let mute = false;
|
||||
let solo = false;
|
||||
let mute = if mute { White } else { track.color.darkest.rgb };
|
||||
let solo = if solo { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) { track.color.light.rgb } else { track.color.base.rgb };
|
||||
let bg2 = if t > 0 { self.tracks()[t].color.base.rgb } else { Reset };
|
||||
Self::wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(mute, bg, "Play "),
|
||||
Tui::fg_bg(solo, bg, "Solo ")))))}),
|
||||
self.button_2("O", "add midi out"));
|
||||
let routes = self.row_top(w, self.h_outputs() - 1,
|
||||
self.io_ports(fg, Tui::g(32), ||self.outputs_sizes()),
|
||||
self.per_track_top(move|t, track|self.io_connections(
|
||||
track.color.dark.rgb, track.color.darker.rgb, ||self.outputs_sizes())), ());
|
||||
|
||||
Align::n(Bsp::s(Bsp::s(nexts, froms), Bsp::s(ports, routes)))
|
||||
}
|
||||
pub(crate) fn h_outputs (&self) -> u16 {
|
||||
1 + self.outputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
pub(crate) fn outputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_outs.iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.conn().len();
|
||||
let data = (i, output.name(), output.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
33
tek/src/view_sizes.rs
Normal file
33
tek/src/view_sizes.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
/// Spacing between tracks.
|
||||
pub(crate) const TRACK_SPACING: usize = 0;
|
||||
/// Default scene height.
|
||||
pub(crate) const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
pub(crate) const H_EDITOR: usize = 15;
|
||||
/// Width taken by all tracks.
|
||||
pub(crate) fn w_tracks (&self) -> u16 {
|
||||
self.tracks_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
|
||||
}
|
||||
/// Width available to display tracks.
|
||||
pub(crate) fn w_tracks_area (&self) -> u16 {
|
||||
self.w().saturating_sub(2 * self.w_sidebar())
|
||||
}
|
||||
/// Height available to display tracks.
|
||||
pub(crate) fn h_tracks_area (&self) -> u16 {
|
||||
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 10)
|
||||
}
|
||||
/// Height taken by all inputs.
|
||||
pub(crate) fn h_inputs (&self) -> u16 {
|
||||
1 + self.inputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all outputs.
|
||||
pub(crate) fn h_outputs (&self) -> u16 {
|
||||
1 + self.outputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all scenes.
|
||||
pub(crate) fn h_scenes (&self) -> u16 {
|
||||
self.scenes_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last().map(|(_, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue