From fe9d5a309e2e9977bda65be193d2035d23ab29d8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 9 Feb 2025 13:59:51 +0100 Subject: [PATCH] relayer arranger view and extract button_2 and button_3 to top level --- midi/src/piano_h.rs | 15 +- plugin/vst/.github/workflows/deploy.yml | 33 + plugin/vst/.github/workflows/docs.yml | 46 + plugin/vst/.github/workflows/rust.yml | 38 + plugin/vst/.gitignore | 21 + plugin/vst/CHANGELOG.md | 86 ++ plugin/vst/Cargo.toml | 75 ++ plugin/vst/LICENSE | 21 + plugin/vst/README.md | 112 ++ plugin/vst/examples/dimension_expander.rs | 222 ++++ plugin/vst/examples/fwd_midi.rs | 71 ++ plugin/vst/examples/gain_effect.rs | 129 +++ plugin/vst/examples/ladder_filter.rs | 248 +++++ plugin/vst/examples/simple_host.rs | 63 ++ plugin/vst/examples/sine_synth.rs | 160 +++ plugin/vst/examples/transfer_and_smooth.rs | 136 +++ plugin/vst/osx_vst_bundler.sh | 61 ++ plugin/vst/rustfmt.toml | 1 + plugin/vst/src/api.rs | 927 ++++++++++++++++ plugin/vst/src/buffer.rs | 606 +++++++++++ plugin/vst/src/cache.rs | 19 + plugin/vst/src/channels.rs | 352 ++++++ plugin/vst/src/editor.rs | 155 +++ plugin/vst/src/event.rs | 133 +++ plugin/vst/src/host.rs | 962 ++++++++++++++++ plugin/vst/src/interfaces.rs | 370 +++++++ plugin/vst/src/lib.rs | 416 +++++++ plugin/vst/src/plugin.rs | 1086 +++++++++++++++++++ plugin/vst/src/prelude.rs | 12 + plugin/vst/src/util/atomic_float.rs | 59 + plugin/vst/src/util/mod.rs | 7 + plugin/vst/src/util/parameter_transfer.rs | 187 ++++ tek/src/cli.rs | 24 +- tek/src/lib.rs | 42 +- tek/src/model.rs | 4 +- tek/src/view.rs | 77 +- tek/src/view_arranger.edn | 5 +- tek/src/{view_clips.rs => view_arranger.rs} | 219 ++-- tek/src/view_color.rs | 22 + tek/src/view_input.rs | 52 - tek/src/view_iter.rs | 77 ++ tek/src/view_output.rs | 50 - tek/src/view_sizes.rs | 33 + 43 files changed, 7158 insertions(+), 276 deletions(-) create mode 100644 plugin/vst/.github/workflows/deploy.yml create mode 100644 plugin/vst/.github/workflows/docs.yml create mode 100644 plugin/vst/.github/workflows/rust.yml create mode 100644 plugin/vst/.gitignore create mode 100644 plugin/vst/CHANGELOG.md create mode 100644 plugin/vst/Cargo.toml create mode 100644 plugin/vst/LICENSE create mode 100644 plugin/vst/README.md create mode 100644 plugin/vst/examples/dimension_expander.rs create mode 100644 plugin/vst/examples/fwd_midi.rs create mode 100644 plugin/vst/examples/gain_effect.rs create mode 100644 plugin/vst/examples/ladder_filter.rs create mode 100644 plugin/vst/examples/simple_host.rs create mode 100644 plugin/vst/examples/sine_synth.rs create mode 100644 plugin/vst/examples/transfer_and_smooth.rs create mode 100755 plugin/vst/osx_vst_bundler.sh create mode 100644 plugin/vst/rustfmt.toml create mode 100644 plugin/vst/src/api.rs create mode 100644 plugin/vst/src/buffer.rs create mode 100644 plugin/vst/src/cache.rs create mode 100644 plugin/vst/src/channels.rs create mode 100644 plugin/vst/src/editor.rs create mode 100644 plugin/vst/src/event.rs create mode 100644 plugin/vst/src/host.rs create mode 100644 plugin/vst/src/interfaces.rs create mode 100755 plugin/vst/src/lib.rs create mode 100644 plugin/vst/src/plugin.rs create mode 100644 plugin/vst/src/prelude.rs create mode 100644 plugin/vst/src/util/atomic_float.rs create mode 100644 plugin/vst/src/util/mod.rs create mode 100644 plugin/vst/src/util/parameter_transfer.rs rename tek/src/{view_clips.rs => view_arranger.rs} (50%) create mode 100644 tek/src/view_color.rs delete mode 100644 tek/src/view_input.rs create mode 100644 tek/src/view_iter.rs delete mode 100644 tek/src/view_output.rs create mode 100644 tek/src/view_sizes.rs diff --git a/midi/src/piano_h.rs b/midi/src/piano_h.rs index f7fa91fd..08414212 100644 --- a/midi/src/piano_h.rs +++ b/midi/src/piano_h.rs @@ -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>>) { *self.clip_mut() = clip.cloned(); diff --git a/plugin/vst/.github/workflows/deploy.yml b/plugin/vst/.github/workflows/deploy.yml new file mode 100644 index 00000000..4636f530 --- /dev/null +++ b/plugin/vst/.github/workflows/deploy.yml @@ -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 \ No newline at end of file diff --git a/plugin/vst/.github/workflows/docs.yml b/plugin/vst/.github/workflows/docs.yml new file mode 100644 index 00000000..6cb04feb --- /dev/null +++ b/plugin/vst/.github/workflows/docs.yml @@ -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 '' > 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 diff --git a/plugin/vst/.github/workflows/rust.yml b/plugin/vst/.github/workflows/rust.yml new file mode 100644 index 00000000..a453c48b --- /dev/null +++ b/plugin/vst/.github/workflows/rust.yml @@ -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 \ No newline at end of file diff --git a/plugin/vst/.gitignore b/plugin/vst/.gitignore new file mode 100644 index 00000000..06b76755 --- /dev/null +++ b/plugin/vst/.gitignore @@ -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 +*~ diff --git a/plugin/vst/CHANGELOG.md b/plugin/vst/CHANGELOG.md new file mode 100644 index 00000000..df61cc20 --- /dev/null +++ b/plugin/vst/CHANGELOG.md @@ -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). diff --git a/plugin/vst/Cargo.toml b/plugin/vst/Cargo.toml new file mode 100644 index 00000000..ae189138 --- /dev/null +++ b/plugin/vst/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "vst" +version = "0.4.0" +edition = "2021" +authors = [ + "Marko Mijalkovic ", + "Boscop", + "Alex Zywicki ", + "doomy ", + "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"] + diff --git a/plugin/vst/LICENSE b/plugin/vst/LICENSE new file mode 100644 index 00000000..29e06b29 --- /dev/null +++ b/plugin/vst/LICENSE @@ -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. diff --git a/plugin/vst/README.md b/plugin/vst/README.md new file mode 100644 index 00000000..a4b28af3 --- /dev/null +++ b/plugin/vst/README.md @@ -0,0 +1,112 @@ +# vst-rs +[![crates.io][crates-img]][crates-url] +[![dependency status](https://deps.rs/repo/github/rustaudio/vst-rs/status.svg)](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 "] + +[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) diff --git a/plugin/vst/examples/dimension_expander.rs b/plugin/vst/examples/dimension_expander.rs new file mode 100644 index 00000000..0fbe008a --- /dev/null +++ b/plugin/vst/examples/dimension_expander.rs @@ -0,0 +1,222 @@ +// author: Marko Mijalkovic + +#[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>, + params: Arc, + 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) { + 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 { + Arc::clone(&self.params) as Arc + } +} + +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); diff --git a/plugin/vst/examples/fwd_midi.rs b/plugin/vst/examples/fwd_midi.rs new file mode 100644 index 00000000..c5818fbc --- /dev/null +++ b/plugin/vst/examples/fwd_midi.rs @@ -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) { + 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) { + 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, + } + } +} diff --git a/plugin/vst/examples/gain_effect.rs b/plugin/vst/examples/gain_effect.rs new file mode 100644 index 00000000..cbe06bd7 --- /dev/null +++ b/plugin/vst/examples/gain_effect.rs @@ -0,0 +1,129 @@ +// author: doomy + +#[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, +} + +/// 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) { + // 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 { + Arc::clone(&self.params) as Arc + } +} + +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); diff --git a/plugin/vst/examples/ladder_filter.rs b/plugin/vst/examples/ladder_filter.rs new file mode 100644 index 00000000..0c6dac81 --- /dev/null +++ b/plugin/vst/examples/ladder_filter.rs @@ -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, + // 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) { + 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 { + Arc::clone(&self.params) as Arc + } +} + +plugin_main!(LadderFilter); diff --git a/plugin/vst/examples/simple_host.rs b/plugin/vst/examples/simple_host.rs new file mode 100644 index 00000000..d8bafbdc --- /dev/null +++ b/plugin/vst/examples/simple_host.rs @@ -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 = 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); +} diff --git a/plugin/vst/examples/sine_synth.rs b/plugin/vst/examples/sine_synth.rs new file mode 100644 index 00000000..81a0475a --- /dev/null +++ b/plugin/vst/examples/sine_synth.rs @@ -0,0 +1,160 @@ +// author: Rob Saunders + +#[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, +} + +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) { + 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); + } + } +} diff --git a/plugin/vst/examples/transfer_and_smooth.rs b/plugin/vst/examples/transfer_and_smooth.rs new file mode 100644 index 00000000..ba50b121 --- /dev/null +++ b/plugin/vst/examples/transfer_and_smooth.rs @@ -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, + states: Vec, + 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 { + Arc::clone(&self.params) as Arc + } + + fn set_sample_rate(&mut self, sample_rate: f32) { + self.sample_rate = sample_rate; + } + + fn process(&mut self, buffer: &mut AudioBuffer) { + // 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); diff --git a/plugin/vst/osx_vst_bundler.sh b/plugin/vst/osx_vst_bundler.sh new file mode 100755 index 00000000..b28d7da4 --- /dev/null +++ b/plugin/vst/osx_vst_bundler.sh @@ -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 " + + + + CFBundleDevelopmentRegion + English + + CFBundleExecutable + $1 + + CFBundleGetInfoString + vst + + CFBundleIconFile + + + CFBundleIdentifier + com.rust-vst.$1 + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundleName + $1 + + CFBundlePackageType + BNDL + + CFBundleVersion + 1.0 + + CFBundleSignature + $((RANDOM % 9999)) + + CSResourcesFileMapped + + + +" > "$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 diff --git a/plugin/vst/rustfmt.toml b/plugin/vst/rustfmt.toml new file mode 100644 index 00000000..866c7561 --- /dev/null +++ b/plugin/vst/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/plugin/vst/src/api.rs b/plugin/vst/src/api.rs new file mode 100644 index 00000000..44a1a78a --- /dev/null +++ b/plugin/vst/src/api.rs @@ -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 { + //FIXME: find a way to do this without resorting to transmuting via a box + &mut *(self.object as *mut Box) + } + + /// 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 { + &(*(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> { + &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)); + 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 { + use self::Supported::*; + + match val { + 1 => Some(Yes), + 0 => Some(Maybe), + -1 => Some(No), + _ => None, + } + } +} + +impl Into 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> { + 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::()`. + 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::()`. + 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::()`. + 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, + 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::() 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 = 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! + } + } + } + } +} diff --git a/plugin/vst/src/buffer.rs b/plugin/vst/src/buffer.rs new file mode 100644 index 00000000..9a32d789 --- /dev/null +++ b/plugin/vst/src/buffer.rs @@ -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 { + 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 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 { + 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 for Outputs<'a, T> { + type Output = [T]; + + fn index(&self, i: usize) -> &Self::Output { + self.get(i) + } +} + +impl<'a, T> IndexMut 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 { + 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::() 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::() 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, + api_events: Vec, // 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::() - (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::() }; 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){ + /// let events: Vec = vec![ + /// // ... + /// ]; + /// self.send_buffer.send_events(&events, &mut self.host); + /// } + /// # } + /// ``` + #[inline(always)] + pub fn send_events, 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, 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 = (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 = (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 + }); + } + } +} diff --git a/plugin/vst/src/cache.rs b/plugin/vst/src/cache.rs new file mode 100644 index 00000000..f6a1fd2e --- /dev/null +++ b/plugin/vst/src/cache.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +use crate::{editor::Editor, prelude::*}; + +pub(crate) struct PluginCache { + pub info: Info, + pub params: Arc, + pub editor: Option>, +} + +impl PluginCache { + pub fn new(info: &Info, params: Arc, editor: Option>) -> Self { + Self { + info: info.clone(), + params, + editor, + } + } +} diff --git a/plugin/vst/src/channels.rs b/plugin/vst/src/channels.rs new file mode 100644 index 00000000..e72879fd --- /dev/null +++ b/plugin/vst/src/channels.rs @@ -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, + active: bool, + arrangement_type: Option, + ) -> 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 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 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 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` as `SpeakerArrangementType` contains extra info about +/// stereo speakers found in the channel flags. +impl From 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), + } + } +} diff --git a/plugin/vst/src/editor.rs b/plugin/vst/src/editor.rs new file mode 100644 index 00000000..923cf873 --- /dev/null +++ b/plugin/vst/src/editor.rs @@ -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, +} diff --git a/plugin/vst/src/event.rs b/plugin/vst/src/event.rs new file mode 100644 index 00000000..580fd54a --- /dev/null +++ b/plugin/vst/src/event.rs @@ -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, + + /// Offset in samples into note from note start, if available. + pub note_offset: Option, + + /// 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), + } + } +} diff --git a/plugin/vst/src/host.rs b/plugin/vst/src/host.rs new file mode 100644 index 00000000..2ed684f6 --- /dev/null +++ b/plugin/vst/src/host.rs @@ -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 { + 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 { + main: PluginMain, + lib: Arc, + host: Arc>, +} + +/// An instance of an externally loaded VST plugin. +#[allow(dead_code)] // To keep `lib` around. +pub struct PluginInstance { + params: Arc, + lib: Arc, + 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, + is_open: bool, +} + +impl EditorInstance { + fn get_rect(&self) -> Option { + 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 PluginLoader { + /// 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>` 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>) -> Result, 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::) + } + + /// 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 { + // 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) -> 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) { + 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) { + 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 = 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 = 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 { + Arc::clone(&self.params) as Arc + } + + fn get_editor(&mut self) -> Option> { + 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 { + // 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 { + // 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(plugin: &mut P) { +/// let mut host_buffer: HostBuffer = 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 { + inputs: Vec<*const T>, + outputs: Vec<*mut T>, +} + +impl HostBuffer { + /// Create a `HostBuffer` for a given number of input and output channels. + pub fn new(input_count: usize, output_count: usize) -> HostBuffer { + 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 { + 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>>` 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( + 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>; + 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>; + 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 = 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]); + } +} diff --git a/plugin/vst/src/interfaces.rs b/plugin/vst/src/interfaces.rs new file mode 100644 index 00000000..6b5261e7 --- /dev/null +++ b/plugin/vst/src/interfaces.rs @@ -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 = + 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() +} diff --git a/plugin/vst/src/lib.rs b/plugin/vst/src/lib.rs new file mode 100755 index 00000000..b0c26de3 --- /dev/null +++ b/plugin/vst/src/lib.rs @@ -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>` 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(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)) 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); + } +} diff --git a/plugin/vst/src/plugin.rs b/plugin/vst/src/plugin.rs new file mode 100644 index 00000000..90a3fb17 --- /dev/null +++ b/plugin/vst/src/plugin.rs @@ -0,0 +1,1086 @@ +//! Plugin specific structures. + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +use std::os::raw::c_void; +use std::ptr; +use std::sync::Arc; + +use crate::{ + api::{self, consts::VST_MAGIC, AEffect, HostCallbackProc, Supported, TimeInfo}, + buffer::AudioBuffer, + channels::ChannelInfo, + editor::Editor, + host::{self, Host}, +}; + +/// Plugin type. Generally either Effect or Synth. +/// +/// Other types are not necessary to build a plugin and are only useful for the host to categorize +/// the plugin. +#[repr(isize)] +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +pub enum Category { + /// Unknown / not implemented + Unknown, + /// Any effect + Effect, + /// VST instrument + Synth, + /// Scope, tuner, spectrum analyser, etc. + Analysis, + /// Dynamics, etc. + Mastering, + /// Panners, etc. + Spacializer, + /// Delays and Reverbs + RoomFx, + /// Dedicated surround processor. + SurroundFx, + /// Denoiser, etc. + Restoration, + /// Offline processing. + OfflineProcess, + /// Contains other plugins. + Shell, + /// Tone generator, etc. + Generator, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +#[doc(hidden)] +pub enum OpCode { + /// Called when plugin is initialized. + Initialize, + /// Called when plugin is being shut down. + Shutdown, + + /// [value]: preset number to change to. + ChangePreset, + /// [return]: current preset number. + GetCurrentPresetNum, + /// [ptr]: char array with new preset name, limited to `consts::MAX_PRESET_NAME_LEN`. + SetCurrentPresetName, + /// [ptr]: char buffer for current preset name, limited to `consts::MAX_PRESET_NAME_LEN`. + GetCurrentPresetName, + + /// [index]: parameter + /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "db", "ms", etc) + GetParameterLabel, + /// [index]: parameter + /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "0.5", "ROOM", etc). + GetParameterDisplay, + /// [index]: parameter + /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "Release", "Gain") + GetParameterName, + + /// Deprecated. + _GetVu, + + /// [opt]: new sample rate. + SetSampleRate, + /// [value]: new maximum block size. + SetBlockSize, + /// [value]: 1 when plugin enabled, 0 when disabled. + StateChanged, + + /// [ptr]: Rect** receiving pointer to editor size. + EditorGetRect, + /// [ptr]: system dependent window pointer, eg HWND on Windows. + EditorOpen, + /// Close editor. No arguments. + EditorClose, + + /// Deprecated. + _EditorDraw, + /// Deprecated. + _EditorMouse, + /// Deprecated. + _EditorKey, + + /// Idle call from host. + EditorIdle, + + /// Deprecated. + _EditorTop, + /// Deprecated. + _EditorSleep, + /// Deprecated. + _EditorIdentify, + + /// [ptr]: pointer for chunk data address (void**). + /// [index]: 0 for bank, 1 for program + GetData, + /// [ptr]: data (void*) + /// [value]: data size in bytes + /// [index]: 0 for bank, 1 for program + SetData, + + /// [ptr]: VstEvents* TODO: Events + ProcessEvents, + /// [index]: param index + /// [return]: 1=true, 0=false + CanBeAutomated, + /// [index]: param index + /// [ptr]: parameter string + /// [return]: true for success + StringToParameter, + + /// Deprecated. + _GetNumCategories, + + /// [index]: program name + /// [ptr]: char buffer for name, limited to `consts::MAX_PRESET_NAME_LEN` + /// [return]: true for success + GetPresetName, + + /// Deprecated. + _CopyPreset, + /// Deprecated. + _ConnectIn, + /// Deprecated. + _ConnectOut, + + /// [index]: input index + /// [ptr]: `VstPinProperties` + /// [return]: 1 if supported + GetInputInfo, + /// [index]: output index + /// [ptr]: `VstPinProperties` + /// [return]: 1 if supported + GetOutputInfo, + /// [return]: `PluginCategory` category. + GetCategory, + + /// Deprecated. + _GetCurrentPosition, + /// Deprecated. + _GetDestinationBuffer, + + /// [ptr]: `VstAudioFile` array + /// [value]: count + /// [index]: start flag + OfflineNotify, + /// [ptr]: `VstOfflineTask` array + /// [value]: count + OfflinePrepare, + /// [ptr]: `VstOfflineTask` array + /// [value]: count + OfflineRun, + + /// [ptr]: `VstVariableIo` + /// [use]: used for variable I/O processing (offline e.g. timestretching) + ProcessVarIo, + /// TODO: implement + /// [value]: input `*mut VstSpeakerArrangement`. + /// [ptr]: output `*mut VstSpeakerArrangement`. + SetSpeakerArrangement, + + /// Deprecated. + _SetBlocksizeAndSampleRate, + + /// Soft bypass (automatable). + /// [value]: 1 = bypass, 0 = nobypass. + SoftBypass, + // [ptr]: buffer for effect name, limited to `kVstMaxEffectNameLen` + GetEffectName, + + /// Deprecated. + _GetErrorText, + + /// [ptr]: buffer for vendor name, limited to `consts::MAX_VENDOR_STR_LEN`. + GetVendorName, + /// [ptr]: buffer for product name, limited to `consts::MAX_PRODUCT_STR_LEN`. + GetProductName, + /// [return]: vendor specific version. + GetVendorVersion, + /// no definition, vendor specific. + VendorSpecific, + /// [ptr]: "Can do" string. + /// [return]: 1 = yes, 0 = maybe, -1 = no. + CanDo, + /// [return]: tail size (e.g. reverb time). 0 is default, 1 means no tail. + GetTailSize, + + /// Deprecated. + _Idle, + /// Deprecated. + _GetIcon, + /// Deprecated. + _SetVewPosition, + + /// [index]: param index + /// [ptr]: `*mut VstParamInfo` //TODO: Implement + /// [return]: 1 if supported + GetParamInfo, + + /// Deprecated. + _KeysRequired, + + /// [return]: 2400 for vst 2.4. + GetApiVersion, + + /// [index]: ASCII char. + /// [value]: `Key` keycode. + /// [opt]: `flags::modifier_key` bitmask. + /// [return]: 1 if used. + EditorKeyDown, + /// [index]: ASCII char. + /// [value]: `Key` keycode. + /// [opt]: `flags::modifier_key` bitmask. + /// [return]: 1 if used. + EditorKeyUp, + /// [value]: 0 = circular, 1 = circular relative, 2 = linear. + EditorSetKnobMode, + + /// [index]: MIDI channel. + /// [ptr]: `*mut MidiProgramName`. //TODO: Implement + /// [return]: number of used programs, 0 = unsupported. + GetMidiProgramName, + /// [index]: MIDI channel. + /// [ptr]: `*mut MidiProgramName`. //TODO: Implement + /// [return]: index of current program. + GetCurrentMidiProgram, + /// [index]: MIDI channel. + /// [ptr]: `*mut MidiProgramCategory`. //TODO: Implement + /// [return]: number of used categories. + GetMidiProgramCategory, + /// [index]: MIDI channel. + /// [return]: 1 if `MidiProgramName` or `MidiKeyName` has changed. //TODO: Implement + HasMidiProgramsChanged, + /// [index]: MIDI channel. + /// [ptr]: `*mut MidiKeyName`. //TODO: Implement + /// [return]: 1 = supported 0 = not. + GetMidiKeyName, + + /// Called before a preset is loaded. + BeginSetPreset, + /// Called after a preset is loaded. + EndSetPreset, + + /// [value]: inputs `*mut VstSpeakerArrangement` //TODO: Implement + /// [ptr]: Outputs `*mut VstSpeakerArrangement` + GetSpeakerArrangement, + /// [ptr]: buffer for plugin name, limited to `consts::MAX_PRODUCT_STR_LEN`. + /// [return]: next plugin's uniqueID. + ShellGetNextPlugin, + + /// No args. Called once before start of process call. This indicates that the process call + /// will be interrupted (e.g. Host reconfiguration or bypass when plugin doesn't support + /// SoftBypass) + StartProcess, + /// No arguments. Called after stop of process call. + StopProcess, + /// [value]: number of samples to process. Called in offline mode before process. + SetTotalSampleToProcess, + /// [value]: pan law `PanLaw`. //TODO: Implement + /// [opt]: gain. + SetPanLaw, + + /// [ptr]: `*mut VstPatchChunkInfo`. //TODO: Implement + /// [return]: -1 = bank cant be loaded, 1 = can be loaded, 0 = unsupported. + BeginLoadBank, + /// [ptr]: `*mut VstPatchChunkInfo`. //TODO: Implement + /// [return]: -1 = bank cant be loaded, 1 = can be loaded, 0 = unsupported. + BeginLoadPreset, + + /// [value]: 0 if 32 bit, anything else if 64 bit. + SetPrecision, + + /// [return]: number of used MIDI Inputs (1-15). + GetNumMidiInputs, + /// [return]: number of used MIDI Outputs (1-15). + GetNumMidiOutputs, +} + +/// A structure representing static plugin information. +#[derive(Clone, Debug)] +pub struct Info { + /// Plugin Name. + pub name: String, + + /// Plugin Vendor. + pub vendor: String, + + /// Number of different presets. + pub presets: i32, + + /// Number of parameters. + pub parameters: i32, + + /// Number of inputs. + pub inputs: i32, + + /// Number of outputs. + pub outputs: i32, + + /// Number of MIDI input channels (1-16), or 0 for the default of 16 channels. + pub midi_inputs: i32, + + /// Number of MIDI output channels (1-16), or 0 for the default of 16 channels. + pub midi_outputs: i32, + + /// Unique plugin ID. Can be registered with Steinberg to prevent conflicts with other plugins. + /// + /// This ID is used to identify a plugin during save and load of a preset and project. + pub unique_id: i32, + + /// Plugin version (e.g. 0001 = `v0.0.0.1`, 1283 = `v1.2.8.3`). + pub version: i32, + + /// Plugin category. Possible values are found in `enums::PluginCategory`. + pub category: Category, + + /// Latency of the plugin in samples. + /// + /// This reports how many samples it takes for the plugin to create an output (group delay). + pub initial_delay: i32, + + /// Indicates that preset data is handled in formatless chunks. + /// + /// If false, host saves and restores plugin by reading/writing parameter data. If true, it is + /// up to the plugin to manage saving preset data by implementing the + /// `{get, load}_{preset, bank}_chunks()` methods. Default is `false`. + pub preset_chunks: bool, + + /// Indicates whether this plugin can process f64 based `AudioBuffer` buffers. + /// + /// Default is `false`. + pub f64_precision: bool, + + /// If this is true, the plugin will not produce sound when the input is silence. + /// + /// Default is `false`. + pub silent_when_stopped: bool, +} + +impl Default for Info { + fn default() -> Info { + Info { + name: "VST".to_string(), + vendor: String::new(), + + presets: 1, // default preset + parameters: 0, + inputs: 2, // Stereo in,out + outputs: 2, + + midi_inputs: 0, + midi_outputs: 0, + + unique_id: 0, // This must be changed. + version: 1, // v0.0.0.1 + + category: Category::Effect, + + initial_delay: 0, + + preset_chunks: false, + f64_precision: false, + silent_when_stopped: false, + } + } +} + +/// Features which are optionally supported by a plugin. These are queried by the host at run time. +#[derive(Debug)] +#[allow(missing_docs)] +pub enum CanDo { + SendEvents, + SendMidiEvent, + ReceiveEvents, + ReceiveMidiEvent, + ReceiveTimeInfo, + Offline, + MidiProgramNames, + Bypass, + ReceiveSysExEvent, + + //Bitwig specific? + MidiSingleNoteTuningChange, + MidiKeyBasedInstrumentControl, + + Other(String), +} + +impl CanDo { + // TODO: implement FromStr + #![allow(clippy::should_implement_trait)] + /// Converts a string to a `CanDo` instance. Any given string that does not match the predefined + /// values will return a `CanDo::Other` value. + pub fn from_str(s: &str) -> CanDo { + use self::CanDo::*; + + match s { + "sendVstEvents" => SendEvents, + "sendVstMidiEvent" => SendMidiEvent, + "receiveVstEvents" => ReceiveEvents, + "receiveVstMidiEvent" => ReceiveMidiEvent, + "receiveVstTimeInfo" => ReceiveTimeInfo, + "offline" => Offline, + "midiProgramNames" => MidiProgramNames, + "bypass" => Bypass, + + "receiveVstSysexEvent" => ReceiveSysExEvent, + "midiSingleNoteTuningChange" => MidiSingleNoteTuningChange, + "midiKeyBasedInstrumentControl" => MidiKeyBasedInstrumentControl, + otherwise => Other(otherwise.to_string()), + } + } +} + +impl Into for CanDo { + fn into(self) -> String { + use self::CanDo::*; + + match self { + SendEvents => "sendVstEvents".to_string(), + SendMidiEvent => "sendVstMidiEvent".to_string(), + ReceiveEvents => "receiveVstEvents".to_string(), + ReceiveMidiEvent => "receiveVstMidiEvent".to_string(), + ReceiveTimeInfo => "receiveVstTimeInfo".to_string(), + Offline => "offline".to_string(), + MidiProgramNames => "midiProgramNames".to_string(), + Bypass => "bypass".to_string(), + + ReceiveSysExEvent => "receiveVstSysexEvent".to_string(), + MidiSingleNoteTuningChange => "midiSingleNoteTuningChange".to_string(), + MidiKeyBasedInstrumentControl => "midiKeyBasedInstrumentControl".to_string(), + Other(other) => other, + } + } +} + +/// Must be implemented by all VST plugins. +/// +/// All methods except `new` and `get_info` provide a default implementation +/// which does nothing and can be safely overridden. +/// +/// At any time, a plugin is in one of two states: *suspended* or *resumed*. +/// While a plugin is in the *suspended* state, various processing parameters, +/// such as the sample rate and block size, can be changed by the host, but no +/// audio processing takes place. While a plugin is in the *resumed* state, +/// audio processing methods and parameter access methods can be called by +/// the host. A plugin starts in the *suspended* state and is switched between +/// the states by the host using the `resume` and `suspend` methods. +/// +/// Hosts call methods of the plugin on two threads: the UI thread and the +/// processing thread. For this reason, the plugin API is separated into two +/// traits: The `Plugin` trait containing setup and processing methods, and +/// the `PluginParameters` trait containing methods for parameter access. +#[cfg_attr( + not(feature = "disable_deprecation_warning"), + deprecated = "This crate has been deprecated. See https://github.com/RustAudio/vst-rs for more information." +)] +#[allow(unused_variables)] +pub trait Plugin: Send { + /// This method must return an `Info` struct. + fn get_info(&self) -> Info; + + /// Called during initialization to pass a `HostCallback` to the plugin. + /// + /// This method can be overridden to set `host` as a field in the plugin struct. + /// + /// # Example + /// + /// ``` + /// // ... + /// # extern crate vst; + /// # #[macro_use] extern crate log; + /// # use vst::plugin::{Plugin, Info}; + /// use vst::plugin::HostCallback; + /// + /// struct ExamplePlugin { + /// host: HostCallback + /// } + /// + /// impl Plugin for ExamplePlugin { + /// fn new(host: HostCallback) -> ExamplePlugin { + /// ExamplePlugin { + /// host + /// } + /// } + /// + /// fn init(&mut self) { + /// info!("loaded with host vst version: {}", self.host.vst_version()); + /// } + /// + /// // ... + /// # fn get_info(&self) -> Info { + /// # Info { + /// # name: "Example Plugin".to_string(), + /// # ..Default::default() + /// # } + /// # } + /// } + /// + /// # fn main() {} + /// ``` + fn new(host: HostCallback) -> Self + where + Self: Sized; + + /// Called when plugin is fully initialized. + /// + /// This method is only called while the plugin is in the *suspended* state. + fn init(&mut self) { + trace!("Initialized vst plugin."); + } + + /// Called when sample rate is changed by host. + /// + /// This method is only called while the plugin is in the *suspended* state. + fn set_sample_rate(&mut self, rate: f32) {} + + /// Called when block size is changed by host. + /// + /// This method is only called while the plugin is in the *suspended* state. + fn set_block_size(&mut self, size: i64) {} + + /// Called to transition the plugin into the *resumed* state. + fn resume(&mut self) {} + + /// Called to transition the plugin into the *suspended* state. + fn suspend(&mut self) {} + + /// Vendor specific handling. + fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize { + 0 + } + + /// Return whether plugin supports specified action. + /// + /// This method is only called while the plugin is in the *suspended* state. + fn can_do(&self, can_do: CanDo) -> Supported { + info!("Host is asking if plugin can: {:?}.", can_do); + Supported::Maybe + } + + /// Get the tail size of plugin when it is stopped. Used in offline processing as well. + fn get_tail_size(&self) -> isize { + 0 + } + + /// Process an audio buffer containing `f32` values. + /// + /// # Example + /// ```no_run + /// # use vst::plugin::{HostCallback, Info, Plugin}; + /// # use vst::buffer::AudioBuffer; + /// # + /// # struct ExamplePlugin; + /// # impl Plugin for ExamplePlugin { + /// # fn new(_host: HostCallback) -> Self { Self } + /// # + /// # fn get_info(&self) -> Info { Default::default() } + /// # + /// // Processor that clips samples above 0.4 or below -0.4: + /// fn process(&mut self, buffer: &mut AudioBuffer){ + /// // For each input and output + /// for (input, output) in buffer.zip() { + /// // For each input sample and output sample in buffer + /// for (in_sample, out_sample) in input.into_iter().zip(output.into_iter()) { + /// *out_sample = if *in_sample > 0.4 { + /// 0.4 + /// } else if *in_sample < -0.4 { + /// -0.4 + /// } else { + /// *in_sample + /// }; + /// } + /// } + /// } + /// # } + /// ``` + /// + /// This method is only called while the plugin is in the *resumed* state. + fn process(&mut self, buffer: &mut AudioBuffer) { + // For each input and output + for (input, output) in buffer.zip() { + // For each input sample and output sample in buffer + for (in_frame, out_frame) in input.iter().zip(output.iter_mut()) { + *out_frame = *in_frame; + } + } + } + + /// Process an audio buffer containing `f64` values. + /// + /// # Example + /// ```no_run + /// # use vst::plugin::{HostCallback, Info, Plugin}; + /// # use vst::buffer::AudioBuffer; + /// # + /// # struct ExamplePlugin; + /// # impl Plugin for ExamplePlugin { + /// # fn new(_host: HostCallback) -> Self { Self } + /// # + /// # fn get_info(&self) -> Info { Default::default() } + /// # + /// // Processor that clips samples above 0.4 or below -0.4: + /// fn process_f64(&mut self, buffer: &mut AudioBuffer){ + /// // For each input and output + /// for (input, output) in buffer.zip() { + /// // For each input sample and output sample in buffer + /// for (in_sample, out_sample) in input.into_iter().zip(output.into_iter()) { + /// *out_sample = if *in_sample > 0.4 { + /// 0.4 + /// } else if *in_sample < -0.4 { + /// -0.4 + /// } else { + /// *in_sample + /// }; + /// } + /// } + /// } + /// # } + /// ``` + /// + /// This method is only called while the plugin is in the *resumed* state. + fn process_f64(&mut self, buffer: &mut AudioBuffer) { + // For each input and output + for (input, output) in buffer.zip() { + // For each input sample and output sample in buffer + for (in_frame, out_frame) in input.iter().zip(output.iter_mut()) { + *out_frame = *in_frame; + } + } + } + + /// Handle incoming events sent from the host. + /// + /// This is always called before the start of `process` or `process_f64`. + /// + /// This method is only called while the plugin is in the *resumed* state. + fn process_events(&mut self, events: &api::Events) {} + + /// Get a reference to the shared parameter object. + fn get_parameter_object(&mut self) -> Arc { + Arc::new(DummyPluginParameters) + } + + /// Get information about an input channel. Only used by some hosts. + fn get_input_info(&self, input: i32) -> ChannelInfo { + ChannelInfo::new( + format!("Input channel {}", input), + Some(format!("In {}", input)), + true, + None, + ) + } + + /// Get information about an output channel. Only used by some hosts. + fn get_output_info(&self, output: i32) -> ChannelInfo { + ChannelInfo::new( + format!("Output channel {}", output), + Some(format!("Out {}", output)), + true, + None, + ) + } + + /// Called one time before the start of process call. + /// + /// This indicates that the process call will be interrupted (due to Host reconfiguration + /// or bypass state when the plug-in doesn't support softBypass). + /// + /// This method is only called while the plugin is in the *resumed* state. + fn start_process(&mut self) {} + + /// Called after the stop of process call. + /// + /// This method is only called while the plugin is in the *resumed* state. + fn stop_process(&mut self) {} + + /// Return handle to plugin editor if supported. + /// The method need only return the object on the first call. + /// Subsequent calls can just return `None`. + /// + /// The editor object will typically contain an `Arc` reference to the parameter + /// object through which it can communicate with the audio processing. + fn get_editor(&mut self) -> Option> { + None + } +} + +/// Parameter object shared between the UI and processing threads. +/// Since access is shared, all methods take `self` by immutable reference. +/// All mutation must thus be performed using thread-safe interior mutability. +#[allow(unused_variables)] +pub trait PluginParameters: Sync { + /// Set the current preset to the index specified by `preset`. + /// + /// This method can be called on the processing thread for automation. + fn change_preset(&self, preset: i32) {} + + /// Get the current preset index. + fn get_preset_num(&self) -> i32 { + 0 + } + + /// Set the current preset name. + fn set_preset_name(&self, name: String) {} + + /// Get the name of the preset at the index specified by `preset`. + fn get_preset_name(&self, preset: i32) -> String { + "".to_string() + } + + /// Get parameter label for parameter at `index` (e.g. "db", "sec", "ms", "%"). + fn get_parameter_label(&self, index: i32) -> String { + "".to_string() + } + + /// Get the parameter value for parameter at `index` (e.g. "1.0", "150", "Plate", "Off"). + fn get_parameter_text(&self, index: i32) -> String { + format!("{:.3}", self.get_parameter(index)) + } + + /// Get the name of parameter at `index`. + fn get_parameter_name(&self, index: i32) -> String { + format!("Param {}", index) + } + + /// Get the value of parameter at `index`. Should be value between 0.0 and 1.0. + fn get_parameter(&self, index: i32) -> f32 { + 0.0 + } + + /// Set the value of parameter at `index`. `value` is between 0.0 and 1.0. + /// + /// This method can be called on the processing thread for automation. + fn set_parameter(&self, index: i32, value: f32) {} + + /// Return whether parameter at `index` can be automated. + fn can_be_automated(&self, index: i32) -> bool { + true + } + + /// Use String as input for parameter value. Used by host to provide an editable field to + /// adjust a parameter value. E.g. "100" may be interpreted as 100hz for parameter. Returns if + /// the input string was used. + fn string_to_parameter(&self, index: i32, text: String) -> bool { + false + } + + /// If `preset_chunks` is set to true in plugin info, this should return the raw chunk data for + /// the current preset. + fn get_preset_data(&self) -> Vec { + Vec::new() + } + + /// If `preset_chunks` is set to true in plugin info, this should return the raw chunk data for + /// the current plugin bank. + fn get_bank_data(&self) -> Vec { + Vec::new() + } + + /// If `preset_chunks` is set to true in plugin info, this should load a preset from the given + /// chunk data. + fn load_preset_data(&self, data: &[u8]) {} + + /// If `preset_chunks` is set to true in plugin info, this should load a preset bank from the + /// given chunk data. + fn load_bank_data(&self, data: &[u8]) {} +} + +struct DummyPluginParameters; + +impl PluginParameters for DummyPluginParameters {} + +/// A reference to the host which allows the plugin to call back and access information. +/// +/// # Panics +/// +/// All methods in this struct will panic if the `HostCallback` was constructed using +/// `Default::default()` rather than being set to the value passed to `Plugin::new`. +#[derive(Copy, Clone)] +pub struct HostCallback { + callback: Option, + effect: *mut AEffect, +} + +/// `HostCallback` implements `Default` so that the plugin can implement `Default` and have a +/// `HostCallback` field. +impl Default for HostCallback { + fn default() -> HostCallback { + HostCallback { + callback: None, + effect: ptr::null_mut(), + } + } +} + +unsafe impl Send for HostCallback {} +unsafe impl Sync for HostCallback {} + +impl HostCallback { + /// Wrap callback in a function to avoid using fn pointer notation. + #[doc(hidden)] + fn callback( + &self, + effect: *mut AEffect, + opcode: host::OpCode, + index: i32, + value: isize, + ptr: *mut c_void, + opt: f32, + ) -> isize { + let callback = self.callback.unwrap_or_else(|| panic!("Host not yet initialized.")); + callback(effect, opcode.into(), index, value, ptr, opt) + } + + /// Check whether the plugin has been initialized. + #[doc(hidden)] + fn is_effect_valid(&self) -> bool { + // Check whether `effect` points to a valid AEffect struct + unsafe { (*self.effect).magic as i32 == VST_MAGIC } + } + + /// Create a new Host structure wrapping a host callback. + #[doc(hidden)] + pub fn wrap(callback: HostCallbackProc, effect: *mut AEffect) -> HostCallback { + HostCallback { + callback: Some(callback), + effect, + } + } + + /// Get the VST API version supported by the host e.g. `2400 = VST 2.4`. + pub fn vst_version(&self) -> i32 { + self.callback(self.effect, host::OpCode::Version, 0, 0, ptr::null_mut(), 0.0) as i32 + } + + /// Get the callback for calling host-specific extensions + #[inline(always)] + pub fn raw_callback(&self) -> Option { + self.callback + } + + /// Get the effect pointer for calling host-specific extensions + #[inline(always)] + pub fn raw_effect(&self) -> *mut AEffect { + self.effect + } + + fn read_string(&self, opcode: host::OpCode, max: usize) -> String { + self.read_string_param(opcode, 0, 0, 0.0, max) + } + + fn read_string_param(&self, opcode: host::OpCode, index: i32, value: isize, opt: f32, max: usize) -> String { + let mut buf = vec![0; max]; + self.callback(self.effect, 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 Host for HostCallback { + /// Signal the host that the value for the parameter has changed. + /// + /// Make sure to also call `begin_edit` and `end_edit` when a parameter + /// has been touched. This is important for the host to determine + /// if a user interaction is happening and the automation should be recorded. + fn automate(&self, index: i32, value: f32) { + if self.is_effect_valid() { + // TODO: Investigate removing this check, should be up to host + self.callback(self.effect, host::OpCode::Automate, index, 0, ptr::null_mut(), value); + } + } + + /// Signal the host the start of a parameter change a gesture (mouse down on knob dragging). + fn begin_edit(&self, index: i32) { + self.callback(self.effect, host::OpCode::BeginEdit, index, 0, ptr::null_mut(), 0.0); + } + + /// Signal the host the end of a parameter change gesture (mouse up after knob dragging). + fn end_edit(&self, index: i32) { + self.callback(self.effect, host::OpCode::EndEdit, index, 0, ptr::null_mut(), 0.0); + } + + fn get_plugin_id(&self) -> i32 { + self.callback(self.effect, host::OpCode::CurrentId, 0, 0, ptr::null_mut(), 0.0) as i32 + } + + fn idle(&self) { + self.callback(self.effect, host::OpCode::Idle, 0, 0, ptr::null_mut(), 0.0); + } + + fn get_info(&self) -> (isize, String, String) { + use api::consts::*; + let version = self.callback(self.effect, host::OpCode::CurrentId, 0, 0, ptr::null_mut(), 0.0) as isize; + let vendor_name = self.read_string(host::OpCode::GetVendorString, MAX_VENDOR_STR_LEN); + let product_name = self.read_string(host::OpCode::GetProductString, MAX_PRODUCT_STR_LEN); + (version, vendor_name, product_name) + } + + /// Send events to the host. + /// + /// This should only be called within [`process`] or [`process_f64`]. Calling `process_events` + /// anywhere else is undefined behaviour and may crash some hosts. + /// + /// [`process`]: trait.Plugin.html#method.process + /// [`process_f64`]: trait.Plugin.html#method.process_f64 + fn process_events(&self, events: &api::Events) { + self.callback( + self.effect, + host::OpCode::ProcessEvents, + 0, + 0, + events as *const _ as *mut _, + 0.0, + ); + } + + /// Request time information from Host. + /// + /// The mask parameter is composed of the same flags which will be found in the `flags` field of `TimeInfo` when returned. + /// That is, if you want the host's tempo, the parameter passed to `get_time_info()` should have the `TEMPO_VALID` flag set. + /// This request and delivery system is important, as a request like this may cause + /// significant calculations at the application's end, which may take a lot of our precious time. + /// This obviously means you should only set those flags that are required to get the information you need. + /// + /// Also please be aware that requesting information does not necessarily mean that that information is provided in return. + /// Check the flags field in the `TimeInfo` structure to see if your request was actually met. + fn get_time_info(&self, mask: i32) -> Option { + let opcode = host::OpCode::GetTime; + let mask = mask as isize; + let null = ptr::null_mut(); + let ptr = self.callback(self.effect, opcode, 0, mask, null, 0.0); + + match ptr { + 0 => None, + ptr => Some(unsafe { *(ptr as *const TimeInfo) }), + } + } + + /// Get block size. + fn get_block_size(&self) -> isize { + self.callback(self.effect, host::OpCode::GetBlockSize, 0, 0, ptr::null_mut(), 0.0) + } + + /// Refresh UI after the plugin's parameters changed. + fn update_display(&self) { + self.callback(self.effect, host::OpCode::UpdateDisplay, 0, 0, ptr::null_mut(), 0.0); + } +} + +#[cfg(test)] +mod tests { + use std::ptr; + + use crate::plugin; + + /// Create a plugin instance. + /// + /// This is a macro to allow you to specify attributes on the created struct. + macro_rules! make_plugin { + ($($attr:meta) *) => { + use std::convert::TryFrom; + use std::os::raw::c_void; + + use crate::main; + use crate::api::AEffect; + use crate::host::{Host, OpCode}; + use crate::plugin::{HostCallback, Info, Plugin}; + + $(#[$attr]) * + struct TestPlugin { + host: HostCallback + } + + impl Plugin for TestPlugin { + fn get_info(&self) -> Info { + Info { + name: "Test Plugin".to_string(), + ..Default::default() + } + } + + fn new(host: HostCallback) -> TestPlugin { + TestPlugin { + host + } + } + + fn init(&mut self) { + info!("Loaded with host vst version: {}", self.host.vst_version()); + assert_eq!(2400, self.host.vst_version()); + assert_eq!(9876, self.host.get_plugin_id()); + // Callback will assert these. + self.host.begin_edit(123); + self.host.automate(123, 12.3); + self.host.end_edit(123); + self.host.idle(); + } + } + + #[allow(dead_code)] + fn instance() -> *mut AEffect { + extern "C" fn host_callback( + _effect: *mut AEffect, + opcode: i32, + index: i32, + _value: isize, + _ptr: *mut c_void, + opt: f32, + ) -> isize { + match OpCode::try_from(opcode) { + Ok(OpCode::BeginEdit) => { + assert_eq!(index, 123); + 0 + }, + Ok(OpCode::Automate) => { + assert_eq!(index, 123); + assert_eq!(opt, 12.3); + 0 + }, + Ok(OpCode::EndEdit) => { + assert_eq!(index, 123); + 0 + }, + Ok(OpCode::Version) => 2400, + Ok(OpCode::CurrentId) => 9876, + Ok(OpCode::Idle) => 0, + _ => 0 + } + } + + main::(host_callback) + } + } + } + + make_plugin!(derive(Default)); + + #[test] + #[should_panic] + fn null_panic() { + make_plugin!(/* no `derive(Default)` */); + + impl Default for TestPlugin { + fn default() -> TestPlugin { + let plugin = TestPlugin { + host: Default::default(), + }; + + // Should panic + let version = plugin.host.vst_version(); + info!("Loaded with host vst version: {}", version); + + plugin + } + } + + TestPlugin::default(); + } + + #[test] + fn host_callbacks() { + let aeffect = instance(); + (unsafe { (*aeffect).dispatcher })(aeffect, plugin::OpCode::Initialize.into(), 0, 0, ptr::null_mut(), 0.0); + } +} diff --git a/plugin/vst/src/prelude.rs b/plugin/vst/src/prelude.rs new file mode 100644 index 00000000..dda5705e --- /dev/null +++ b/plugin/vst/src/prelude.rs @@ -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}; diff --git a/plugin/vst/src/util/atomic_float.rs b/plugin/vst/src/util/atomic_float.rs new file mode 100644 index 00000000..e1cce2df --- /dev/null +++ b/plugin/vst/src/util/atomic_float.rs @@ -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 for AtomicFloat { + fn from(value: f32) -> Self { + AtomicFloat::new(value) + } +} + +impl From for f32 { + fn from(value: AtomicFloat) -> Self { + value.get() + } +} diff --git a/plugin/vst/src/util/mod.rs b/plugin/vst/src/util/mod.rs new file mode 100644 index 00000000..fbe7a87e --- /dev/null +++ b/plugin/vst/src/util/mod.rs @@ -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}; diff --git a/plugin/vst/src/util/parameter_transfer.rs b/plugin/vst/src/util/parameter_transfer.rs new file mode 100644 index 00000000..37ebc92b --- /dev/null +++ b/plugin/vst/src/util/parameter_transfer.rs @@ -0,0 +1,187 @@ +use std::mem::size_of; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; + +const USIZE_BITS: usize = size_of::() * 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, + changed: Vec, +} + +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])); + } + } +} diff --git a/tek/src/cli.rs b/tek/src/cli.rs index 9fc7f160..8d131239 100644 --- a/tek/src/cli.rs +++ b/tek/src/cli.rs @@ -163,7 +163,7 @@ impl Tek { midi_froms: &[PortConnect], midi_tos: &[PortConnect], audio_froms: &[&[PortConnect];2], audio_tos: &[&[PortConnect];2], ) -> Usually { - 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 { - 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 () { diff --git a/tek/src/lib.rs b/tek/src/lib.rs index e2b64515..90c84425 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -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 = std::result::Result>; /// Standard optional result type. diff --git a/tek/src/model.rs b/tek/src/model.rs index 977781bd..aed7608e 100644 --- a/tek/src/model.rs +++ b/tek/src/model.rs @@ -10,8 +10,8 @@ use crate::*; pub pool: Option, /// Contains the currently edited MIDI clip pub editor: Option, - /// Contains the project arrangement - pub arranger: BigBuffer, + /// Contains a render of the project arrangement, redrawn on update. + pub arranger: Arc>, pub midi_ins: Vec, pub midi_outs: Vec, pub audio_ins: Vec, diff --git a/tek/src/view.rs b/tek/src/view.rs index 2ea88f95..084002ab 100644 --- a/tek/src/view.rs +++ b/tek/src/view.rs @@ -104,37 +104,56 @@ impl Tek { content: impl Content + Send + Sync + 'a ) -> impl Content + '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 + 'a - where K: Content + 'a, L: Content + '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 + 'a - where K: Content + 'a, L: Content + 'a, V: Content + '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) -> impl Content { 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 + 'a where + K: Content + 'a, + L: Content + '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 + 'a where + K: Content + 'a, + L: Content + 'a, + V: Content + '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, ""); } diff --git a/tek/src/view_arranger.edn b/tek/src/view_arranger.edn index f8431aa4..a279383a 100644 --- a/tek/src/view_arranger.edn +++ b/tek/src/view_arranger.edn @@ -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)))) diff --git a/tek/src/view_clips.rs b/tek/src/view_arranger.rs similarity index 50% rename from tek/src/view_clips.rs rename to tek/src/view_arranger.rs index 21989cb7..5be59755 100644 --- a/tek/src/view_clips.rs +++ b/tek/src/view_arranger.rs @@ -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 + 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 + 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 + 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 + 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 + 'a> ( @@ -93,8 +47,7 @@ impl Tek { ) -> impl Content + 'a { self.per_track_top(move|index, track|Fill::y(Align::y(f(index, track)))) } - - pub(crate) fn per_track_top <'a, T: Content + 'a> ( + fn per_track_top <'a, T: Content + 'a> ( &'a self, f: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Content + 'a { let width = self.w_tracks_area(); @@ -112,10 +65,11 @@ impl Tek { pub fn view_tracks (&self) -> impl Content + 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 + use<'a> { @@ -162,9 +116,17 @@ impl Tek { } fn view_scene_clip ( - &self, width: u16, height: u16, offset: u16, - scene: &Scene, prev: Option, s: usize, t: usize, - editing: bool, same_track: bool, selected_scene: Option + &self, + width: u16, + height: u16, + offset: u16, + scene: &Scene, + prev: Option, + s: usize, + t: usize, + editing: bool, + same_track: bool, + selected_scene: Option ) -> impl Content + 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 + 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 + 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 + 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, - 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, 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 } } -} diff --git a/tek/src/view_color.rs b/tek/src/view_color.rs new file mode 100644 index 00000000..76db469f --- /dev/null +++ b/tek/src/view_color.rs @@ -0,0 +1,22 @@ +use crate::*; +impl Tek { + pub(crate) fn colors ( + theme: &ItemPalette, + prev: Option, + 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, 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 } + } +} diff --git a/tek/src/view_input.rs b/tek/src/view_input.rs deleted file mode 100644 index 76d6c101..00000000 --- a/tek/src/view_input.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; -impl Tek { - pub fn view_inputs (&self) -> impl Content + 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 - }) - } -} diff --git a/tek/src/view_iter.rs b/tek/src/view_iter.rs new file mode 100644 index 00000000..779de1e9 --- /dev/null +++ b/tek/src/view_iter.rs @@ -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])) + })) + }) + } +} diff --git a/tek/src/view_output.rs b/tek/src/view_output.rs deleted file mode 100644 index f91af965..00000000 --- a/tek/src/view_output.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::*; -impl Tek { - pub fn view_outputs (&self) -> impl Content + 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 - }) - } -} - diff --git a/tek/src/view_sizes.rs b/tek/src/view_sizes.rs new file mode 100644 index 00000000..76299516 --- /dev/null +++ b/tek/src/view_sizes.rs @@ -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) + } +}