├── .cargo └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── gain-plugin │ ├── Cargo.toml │ ├── build.rs │ ├── src │ ├── editor.rs │ ├── lib.rs │ ├── parameters.rs │ ├── plugin.rs │ ├── processor.rs │ └── view.rs │ └── ui │ └── main.slint ├── plinth-core ├── Cargo.toml └── src │ ├── buffers.rs │ ├── buffers │ └── buffer.rs │ ├── collections.rs │ ├── collections │ ├── copy_from_slice.rs │ └── interleave_iterator.rs │ ├── lib.rs │ ├── signals.rs │ ├── signals │ ├── channels.rs │ ├── frame.rs │ ├── frame_iterator.rs │ ├── frames_iterator.rs │ ├── ptr_signal.rs │ ├── signal.rs │ ├── signal_base.rs │ ├── signal_frame.rs │ └── slice.rs │ ├── util.rs │ └── util │ ├── ptr.rs │ └── range.rs ├── plinth-derive ├── Cargo.toml └── src │ ├── enums.rs │ ├── kind.rs │ └── lib.rs ├── plinth-plugin ├── Cargo.toml ├── build.rs ├── include │ └── plinth_auv3.h └── src │ ├── editor.rs │ ├── error.rs │ ├── event.rs │ ├── formats.rs │ ├── formats │ ├── auv3.rs │ ├── auv3 │ │ ├── au_render_event.rs │ │ ├── event.rs │ │ ├── host.rs │ │ ├── macros.rs │ │ ├── parameters.rs │ │ ├── plugin.rs │ │ ├── reader.rs │ │ ├── util.rs │ │ ├── wrapper.rs │ │ └── writer.rs │ ├── clap.rs │ ├── clap │ │ ├── descriptor.rs │ │ ├── entry_point.rs │ │ ├── event.rs │ │ ├── extensions.rs │ │ ├── extensions │ │ │ ├── audio_ports.rs │ │ │ ├── gui.rs │ │ │ ├── latency.rs │ │ │ ├── note_ports.rs │ │ │ ├── params.rs │ │ │ ├── render.rs │ │ │ ├── state.rs │ │ │ ├── tail.rs │ │ │ └── timer_support.rs │ │ ├── factory.rs │ │ ├── features.rs │ │ ├── host.rs │ │ ├── macros.rs │ │ ├── parameters.rs │ │ ├── plugin.rs │ │ ├── plugin_instance.rs │ │ ├── stream.rs │ │ └── transport.rs │ ├── vst3.rs │ └── vst3 │ │ ├── component.rs │ │ ├── error.rs │ │ ├── event.rs │ │ ├── factory.rs │ │ ├── host.rs │ │ ├── macros.rs │ │ ├── parameters.rs │ │ ├── plugin.rs │ │ ├── stream.rs │ │ ├── subcategories.rs │ │ ├── transport.rs │ │ └── view.rs │ ├── host.rs │ ├── lib.rs │ ├── parameters.rs │ ├── parameters │ ├── bool.rs │ ├── enums.rs │ ├── float.rs │ ├── formatter.rs │ ├── group.rs │ ├── info.rs │ ├── int.rs │ ├── kind.rs │ ├── map.rs │ ├── parameter.rs │ └── range.rs │ ├── plugin.rs │ ├── processor.rs │ ├── string.rs │ ├── transport.rs │ └── window_handle.rs ├── plugin-canvas-slint ├── Cargo.toml └── src │ ├── editor.rs │ ├── lib.rs │ ├── platform.rs │ ├── view.rs │ └── window_adapter.rs ├── plugin-canvas ├── Cargo.toml ├── README.md └── src │ ├── dimensions.rs │ ├── drag_drop.rs │ ├── error.rs │ ├── event.rs │ ├── keyboard.rs │ ├── lib.rs │ ├── platform.rs │ ├── platform │ ├── interface.rs │ ├── mac.rs │ ├── mac │ │ ├── keyboard.rs │ │ ├── view.rs │ │ └── window.rs │ ├── os_window_handle.rs │ ├── win32.rs │ ├── win32 │ │ ├── cursors.rs │ │ ├── drop_target.rs │ │ ├── keyboard.rs │ │ ├── message_window.rs │ │ ├── version.rs │ │ └── window.rs │ ├── x11.rs │ └── x11 │ │ ├── cursors.rs │ │ ├── keyboard.rs │ │ └── window.rs │ └── window.rs └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --release --" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | target 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "examples/*", 5 | "plinth-core", 6 | "plinth-derive", 7 | "plinth-plugin", 8 | "plugin-canvas", 9 | "plugin-canvas-slint", 10 | "xtask", 11 | ] 12 | 13 | [workspace.dependencies] 14 | plinth-core = { path = "plinth-core" } 15 | plinth-derive = { path = "plinth-derive" } 16 | plinth-plugin = { path = "plinth-plugin" } 17 | plugin-canvas = { path = "plugin-canvas" } 18 | plugin-canvas-slint = { path = "plugin-canvas-slint" } 19 | 20 | cursor-icon = "1.1" 21 | keyboard-types = "0.7" 22 | log = "0.4" 23 | num-traits = "0.2" 24 | portable-atomic = { version = "1.10", features = ["float", "serde"] } 25 | raw-window-handle = "0.6" 26 | slint = { version = "1.11.0", default-features = false, features = ["accessibility", "compat-1-2", "std"] } 27 | 28 | # Internal slint crate versions need to be pinned 29 | # since they don't maintain semver compatibility 30 | i-slint-common = "1.11.0" 31 | i-slint-core = "1.11.0" 32 | i-slint-renderer-skia = { version = "1.11.0", features = ["x11"] } 33 | 34 | [patch.crates-io] 35 | # FIXME: Needed for loading cursors to work, remove once the fix has shipped 36 | x11rb = { git = "https://github.com/psychon/x11rb" } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jussi Viiri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | plinth-core is a support crate for plinth-plugin 2 | 3 | plinth-derive contains derive macros for plinth-plugin 4 | 5 | plinth-plugin is an opinionated audio plugin format abstraction crate for AUv3, CLAP and VST3 6 | 7 | plugin-canvas is an opinionated windowing abstraction crate for audio plugins 8 | 9 | plugin-canvas-slint allows opening slint windows in an audio plugin context using plugin-canvas 10 | -------------------------------------------------------------------------------- /examples/gain-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain-plugin" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "lib", "staticlib"] 8 | 9 | [dependencies] 10 | plinth-derive.workspace = true 11 | plinth-plugin.workspace = true 12 | plugin-canvas-slint.workspace = true 13 | serde = "1.0" 14 | serde_json = "1.0" 15 | slint = { version = "1.10", default-features = false, features = ["accessibility", "compat-1-2", "std"] } 16 | 17 | [build-dependencies] 18 | slint-build = "1.10" 19 | -------------------------------------------------------------------------------- /examples/gain-plugin/build.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::PathBuf}; 2 | 3 | fn main() -> Result<(), Box> { 4 | let config = slint_build::CompilerConfiguration::new() 5 | .with_include_paths(vec![ 6 | PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("ui/"), 7 | ]); 8 | 9 | slint_build::compile_with_config("ui/main.slint", config)?; 10 | 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/editor.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use plinth_plugin::{raw_window_handle::RawWindowHandle, Editor, Host}; 4 | use plugin_canvas_slint::{editor::{EditorHandle, SlintEditor}, plugin_canvas::window::WindowAttributes}; 5 | 6 | use crate::{parameters::GainParameters, view::GainPluginView}; 7 | 8 | pub struct GainPluginEditor { 9 | host: Rc, 10 | editor_handle: Option>, 11 | parameters: Rc, 12 | } 13 | 14 | impl GainPluginEditor { 15 | pub fn new(host: Rc, parameters: Rc) -> Self { 16 | Self { 17 | host, 18 | editor_handle: None, 19 | parameters, 20 | } 21 | } 22 | } 23 | 24 | impl Editor for GainPluginEditor { 25 | const DEFAULT_SIZE: (f64, f64) = (400.0, 300.0); 26 | 27 | fn open(&mut self, parent: RawWindowHandle) { 28 | // Drop old editor instance first 29 | self.close(); 30 | 31 | let editor_handle = SlintEditor::open( 32 | parent, 33 | WindowAttributes::new(Self::DEFAULT_SIZE.into(), 1.0), 34 | { 35 | let parameters = self.parameters.clone(); 36 | let host = self.host.clone(); 37 | 38 | move |_| { 39 | GainPluginView::new(parameters.clone(), host.clone()) 40 | } 41 | }, 42 | ); 43 | 44 | self.editor_handle = Some(editor_handle); 45 | } 46 | 47 | fn close(&mut self) { 48 | self.editor_handle = None; 49 | } 50 | 51 | fn on_frame(&mut self) { 52 | if let Some(editor_handle) = self.editor_handle.as_ref() { 53 | editor_handle.on_frame(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod editor; 2 | mod parameters; 3 | mod plugin; 4 | mod processor; 5 | mod view; 6 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/parameters.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use plinth_derive::ParameterKind; 4 | use plinth_plugin::{FloatFormatter, FloatParameter, LinearFloatRange, Parameter, ParameterId, ParameterMap, Parameters}; 5 | 6 | const MIN_GAIN: f64 = -80.0; 7 | const MAX_GAIN: f64 = 80.0; 8 | 9 | #[derive(ParameterKind)] 10 | pub enum GainParameter { 11 | Gain, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub struct GainParameters { 16 | map: ParameterMap, 17 | } 18 | 19 | impl Default for GainParameters { 20 | fn default() -> Self { 21 | let mut map = ParameterMap::new(); 22 | 23 | map.add( 24 | FloatParameter::new( 25 | GainParameter::Gain, 26 | "Gain", 27 | Arc::new(LinearFloatRange::new(MIN_GAIN, MAX_GAIN)), 28 | ) 29 | .with_default_value(0.0) 30 | .with_formatter(Arc::new(FloatFormatter::new(1, "dB"))) 31 | ); 32 | 33 | Self { 34 | map, 35 | } 36 | } 37 | } 38 | 39 | impl Parameters for GainParameters { 40 | fn ids(&self) -> &[ParameterId] { 41 | self.map.ids() 42 | } 43 | 44 | fn get(&self, id: impl Into) -> Option<&dyn Parameter> { 45 | self.map.get(id) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{Read, Write}; 3 | use std::rc::Rc; 4 | 5 | use plinth_plugin::error::Error; 6 | use plinth_plugin::{export_clap, export_vst3, Event, Host, HostInfo, Parameters, Plugin, ProcessorConfig}; 7 | use plinth_plugin::clap::ClapPlugin; 8 | use plinth_plugin::vst3::Vst3Plugin; 9 | 10 | use crate::editor::GainPluginEditor; 11 | use crate::{parameters::GainParameters, processor::GainPluginProcessor}; 12 | 13 | #[derive(Default)] 14 | struct GainPlugin { 15 | parameters: Rc, 16 | } 17 | 18 | impl Plugin for GainPlugin { 19 | const NAME: &'static str = "Gain Example"; 20 | const VENDOR: &'static str = "Viiri Audio"; 21 | const VERSION: &'static str = "0.1"; 22 | 23 | type Processor = GainPluginProcessor; 24 | type Editor = GainPluginEditor; 25 | type Parameters = GainParameters; 26 | 27 | fn new(_host_info: HostInfo) -> Self { 28 | Self::default() 29 | } 30 | 31 | fn with_parameters(&self, mut f: impl FnMut(&Self::Parameters) -> T) -> T { 32 | f(&self.parameters) 33 | } 34 | 35 | fn process_event(&mut self, event: &Event) { 36 | self.parameters.process_event(event); 37 | } 38 | 39 | fn create_processor(&mut self, _config: ProcessorConfig) -> Self::Processor { 40 | GainPluginProcessor::new((*self.parameters).clone()) 41 | } 42 | 43 | fn create_editor(&mut self, host: Rc) -> Self::Editor { 44 | GainPluginEditor::new(host, self.parameters.clone()) 45 | } 46 | 47 | fn save_state(&self, writer: &mut impl Write) -> Result<(), Error> { 48 | let serialized_parameters: HashMap<_, _> = self.parameters.serialize().collect(); 49 | let parameters_json = serde_json::to_string(&serialized_parameters) 50 | .map_err(|_| Error::SerializationError)?; 51 | write!(writer, "{parameters_json}")?; 52 | 53 | Ok(()) 54 | } 55 | 56 | fn load_state(&mut self, reader: &mut impl Read) -> Result<(), Error> { 57 | let mut parameters_json = String::new(); 58 | reader.read_to_string(&mut parameters_json)?; 59 | 60 | let serialized_parameters: HashMap<_, _> = serde_json::from_str(¶meters_json) 61 | .map_err(|_| Error::SerializationError)?; 62 | self.parameters.deserialize(serialized_parameters)?; 63 | 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl ClapPlugin for GainPlugin { 69 | const CLAP_ID: &'static str = "viiri-audio.gain-example"; 70 | const FEATURES: &'static [plinth_plugin::clap::Feature] = &[ 71 | plinth_plugin::clap::Feature::AudioEffect, 72 | plinth_plugin::clap::Feature::Stereo, 73 | ]; 74 | } 75 | 76 | impl Vst3Plugin for GainPlugin { 77 | const CLASS_ID: u128 = 0xE84410DB1788DC81; 78 | const SUBCATEGORIES: &'static [plinth_plugin::vst3::Subcategory] = &[ 79 | plinth_plugin::vst3::Subcategory::Fx, 80 | plinth_plugin::vst3::Subcategory::Stereo, 81 | ]; 82 | } 83 | 84 | export_clap!(GainPlugin); 85 | export_vst3!(GainPlugin); 86 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/processor.rs: -------------------------------------------------------------------------------- 1 | use plinth_plugin::{plinth_core::signals::signal::{Signal, SignalMut}, Event, FloatParameter, Parameters, ProcessState, Processor, Transport}; 2 | 3 | use crate::parameters::{GainParameter, GainParameters}; 4 | 5 | fn db_to_amplitude(db: f32) -> f32 { 6 | 10.0_f32.powf(db / 20.0) 7 | } 8 | 9 | pub struct GainPluginProcessor { 10 | parameters: GainParameters, 11 | } 12 | 13 | impl GainPluginProcessor { 14 | pub fn new(parameters: GainParameters) -> Self { 15 | Self { 16 | parameters, 17 | } 18 | } 19 | } 20 | 21 | impl Processor for GainPluginProcessor { 22 | fn reset(&mut self) { 23 | } 24 | 25 | fn process( 26 | &mut self, 27 | buffer: &mut impl SignalMut, 28 | _aux: Option<&impl Signal>, 29 | _transport: Option, 30 | events: impl Iterator 31 | ) -> ProcessState { 32 | for event in events { 33 | self.parameters.process_event(&event); 34 | } 35 | 36 | let gain_db = self.parameters.value::(GainParameter::Gain); 37 | let gain = db_to_amplitude(gain_db as _); 38 | 39 | for channel in buffer.iter_channels_mut() { 40 | for sample in channel.iter_mut() { 41 | *sample *= gain; 42 | } 43 | } 44 | 45 | ProcessState::Normal 46 | } 47 | 48 | fn process_events(&mut self, events: impl Iterator) { 49 | for event in events { 50 | self.parameters.process_event(&event); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/gain-plugin/src/view.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use plinth_plugin::{FloatParameter, Host, Parameter, Parameters}; 4 | use plugin_canvas_slint::{plugin_canvas::{event::EventResponse, Event}, view::PluginView}; 5 | 6 | use crate::parameters::{GainParameter, GainParameters}; 7 | 8 | slint::include_modules!(); 9 | 10 | pub struct GainPluginView { 11 | plugin_window: PluginWindow, 12 | parameters: Rc, 13 | } 14 | 15 | impl GainPluginView { 16 | pub fn new(parameters: Rc, host: Rc) -> Self { 17 | let plugin_window = PluginWindow::new().unwrap(); 18 | 19 | plugin_window.on_start_parameter_change({ 20 | let host = host.clone(); 21 | 22 | move |id| { 23 | host.start_parameter_change(id as _); 24 | } 25 | }); 26 | 27 | plugin_window.on_change_parameter_value({ 28 | let host = host.clone(); 29 | 30 | move |id, value| { 31 | host.change_parameter_value(id as _, value as _); 32 | } 33 | }); 34 | 35 | plugin_window.on_end_parameter_change({ 36 | let host = host.clone(); 37 | 38 | move |id| { 39 | host.end_parameter_change(id as _); 40 | } 41 | }); 42 | 43 | Self { 44 | plugin_window, 45 | parameters, 46 | } 47 | } 48 | } 49 | 50 | impl PluginView for GainPluginView { 51 | fn window(&self) -> &slint::Window { 52 | self.plugin_window.window() 53 | } 54 | 55 | fn on_event(&self, event: &Event) -> EventResponse { 56 | #[expect(clippy::single_match)] 57 | match event { 58 | Event::Draw => { 59 | let gain_parameter = self.parameters.typed::(GainParameter::Gain).unwrap(); 60 | 61 | self.plugin_window.set_gain(UiParameter { 62 | id: gain_parameter.info().id() as _, 63 | normalized_value: gain_parameter.normalized_value() as _, 64 | default_normalized_value: gain_parameter.info().default_normalized_value() as _, 65 | display_value: gain_parameter.to_string().into(), 66 | }); 67 | } 68 | 69 | _ => {} 70 | } 71 | 72 | EventResponse::Ignored 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/gain-plugin/ui/main.slint: -------------------------------------------------------------------------------- 1 | export struct UiParameter { 2 | id: int, 3 | normalized-value: float, 4 | default-normalized-value: float, 5 | display-value: string, 6 | } 7 | 8 | export component PluginWindow inherits Window { 9 | width: 400px; 10 | height: 300px; 11 | 12 | in property gain; 13 | 14 | callback start-parameter-change(int); 15 | callback change-parameter-value(int, float); 16 | callback end-parameter-change(int); 17 | 18 | Text { 19 | text: gain.display-value; 20 | font-size: 40px; 21 | 22 | TouchArea { 23 | property sensitivity: 0.003; 24 | property mouse-pos: self.mouse-y; 25 | property absolute-mouse-pos: (self.height - self.mouse-y) / self.height; 26 | property last-mouse-pos; 27 | property mouse-delta: mouse-pos - last-mouse-pos; 28 | property value; 29 | 30 | double-clicked => { 31 | start-parameter-change(gain.id); 32 | change-parameter-value(gain.id, gain.default-normalized-value); 33 | end-parameter-change(gain.id); 34 | } 35 | 36 | pointer-event(event) => { 37 | if self.enabled && event.button == PointerEventButton.left { 38 | if event.kind == PointerEventKind.down { 39 | start-parameter-change(gain.id); 40 | value = gain.normalized-value; 41 | last-mouse-pos = mouse-pos; 42 | } else if event.kind == PointerEventKind.up || event.kind == PointerEventKind.cancel { 43 | end-parameter-change(gain.id); 44 | } 45 | } 46 | } 47 | 48 | moved => { 49 | if self.enabled { 50 | value = clamp(value - mouse-delta * sensitivity / 1px, 0.0, 1.0); 51 | change-parameter-value(gain.id, value); 52 | last-mouse-pos = mouse-pos; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plinth-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plinth-core" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | authors = ["Jussi Viiri "] 7 | readme = "README.md" 8 | repository = "https://github.com/ilmai/plugin-things" 9 | license = "MIT" 10 | 11 | [features] 12 | serde = ["dep:serde"] 13 | 14 | [dependencies] 15 | itertools = "0.14" 16 | num-traits.workspace = true 17 | serde = { version = "1.0", features = ["serde_derive"], optional = true } 18 | -------------------------------------------------------------------------------- /plinth-core/src/buffers.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; -------------------------------------------------------------------------------- /plinth-core/src/buffers/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::signals::{signal::Signal, signal_base::{SignalBase, SignalMutBase}}; 2 | 3 | #[derive(Clone, Debug)] 4 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 5 | pub struct Buffer { 6 | samples: Vec>, 7 | } 8 | 9 | impl Buffer { 10 | pub fn new(channels: usize, length: usize) -> Self { 11 | assert!(channels > 0); 12 | 13 | Self { 14 | samples: vec![vec![0.0; length]; channels], 15 | } 16 | } 17 | 18 | pub fn with_capacity(channels: usize, capacity: usize) -> Self { 19 | assert!(channels > 0); 20 | 21 | Self { 22 | samples: vec![Vec::with_capacity(capacity); channels], 23 | } 24 | } 25 | 26 | pub fn from_signal(signal: &impl Signal) -> Self { 27 | let samples: Vec<_> = signal.iter_channels() 28 | .map(|channel| channel.to_vec()) 29 | .collect(); 30 | 31 | Self { 32 | samples, 33 | } 34 | } 35 | 36 | pub fn capacity(&self) -> usize { 37 | self.samples[0].capacity() 38 | } 39 | 40 | pub fn resize(&mut self, length: usize) { 41 | for channel in self.samples.iter_mut() { 42 | channel.resize(length, 0.0); 43 | } 44 | } 45 | 46 | pub fn reserve(&mut self, additional: usize) { 47 | for channel in self.samples.iter_mut() { 48 | channel.reserve(additional); 49 | } 50 | } 51 | } 52 | 53 | impl From>> for Buffer { 54 | fn from(value: Vec>) -> Self { 55 | Buffer { 56 | samples: value, 57 | } 58 | } 59 | } 60 | 61 | impl SignalBase for Buffer { 62 | fn channels(&self) -> usize { 63 | self.samples.len() 64 | } 65 | 66 | fn len(&self) -> usize { 67 | self.samples[0].len() 68 | } 69 | 70 | fn channel_ptr(&self, channel: usize) -> *const [f32] { 71 | self.samples[channel].as_slice() 72 | } 73 | } 74 | 75 | impl SignalMutBase for Buffer { 76 | fn channel_ptr_mut(&mut self, channel: usize) -> *mut [f32] { 77 | self.samples[channel].as_mut_slice() 78 | } 79 | } 80 | 81 | impl PartialEq for Buffer { 82 | fn eq(&self, other: &Buffer) -> bool { 83 | self.samples == other.samples 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use crate::signals::{signal::{Signal, SignalMut}, signal_base::SignalBase}; 90 | 91 | use super::Buffer; 92 | 93 | #[test] 94 | fn create() { 95 | let buffer = Buffer::new(1, 2); 96 | assert_eq!(buffer.iter_channels().count(), 1); 97 | assert_eq!(buffer.channel(0), &[0.0, 0.0]); 98 | } 99 | 100 | #[test] 101 | fn read_write_1_channel_2_samples() { 102 | let mut buffer = Buffer::new(1, 2); 103 | buffer.channel_mut(0).copy_from_slice(&[1.0, 2.0]); 104 | assert_eq!(buffer.channel(0), [1.0, 2.0]); 105 | } 106 | 107 | #[test] 108 | fn copy_from_signal() { 109 | let mut buffer1 = Buffer::new(2, 2); 110 | let mut buffer2 = Buffer::new(2, 2); 111 | 112 | buffer2.channel_mut(0).copy_from_slice(&[1.0, 2.0]); 113 | buffer2.channel_mut(1).copy_from_slice(&[3.0, 4.0]); 114 | 115 | buffer1.copy_from_signal(&buffer2); 116 | 117 | assert_eq!(buffer1, buffer2); 118 | } 119 | 120 | #[test] 121 | fn apply_wrap() { 122 | let mut buffer1 = Buffer::new(2, 2); 123 | let mut buffer2 = Buffer::new(2, 2); 124 | 125 | buffer2.channel_mut(0).copy_from_slice(&[1.0, 2.0]); 126 | buffer2.channel_mut(1).copy_from_slice(&[3.0, 4.0]); 127 | 128 | let buffer2_len = buffer2.len(); 129 | buffer2.apply_wrap(1, buffer2_len, |signal, range| buffer1.slice_mut(range).copy_from_signal(signal)); 130 | 131 | assert_eq!(buffer1.channel(0), &[2.0, 1.0]); 132 | assert_eq!(buffer1.channel(1), &[4.0, 3.0]); 133 | } 134 | 135 | #[test] 136 | fn apply_wrap_mut() { 137 | let mut buffer1 = Buffer::new(2, 2); 138 | let mut buffer2 = Buffer::new(2, 2); 139 | 140 | buffer2.channel_mut(0).copy_from_slice(&[1.0, 2.0]); 141 | buffer2.channel_mut(1).copy_from_slice(&[3.0, 4.0]); 142 | 143 | let buffer2_len = buffer2.len(); 144 | buffer1.apply_wrap_mut(1, buffer2_len, |signal, range| signal.copy_from_signal(&buffer2.slice(range))); 145 | 146 | assert_eq!(buffer1.channel(0), &[2.0, 1.0]); 147 | assert_eq!(buffer1.channel(1), &[4.0, 3.0]); 148 | } 149 | 150 | #[test] 151 | fn truncate() { 152 | let mut buffer = Buffer::new(3, 4); 153 | buffer.channel_mut(0).copy_from_slice(&[1.0, 2.0, 3.0, 4.0]); 154 | buffer.channel_mut(1).copy_from_slice(&[5.0, 6.0, 7.0, 8.0]); 155 | buffer.channel_mut(2).copy_from_slice(&[9.0, 10.0, 11.0, 12.0]); 156 | 157 | buffer.resize(2); 158 | assert_eq!(buffer.channel(0), [1.0, 2.0]); 159 | assert_eq!(buffer.channel(1), [5.0, 6.0]); 160 | assert_eq!(buffer.channel(2), [9.0, 10.0]); 161 | } 162 | 163 | #[test] 164 | fn grow() { 165 | let mut buffer = Buffer::new(3, 2); 166 | buffer.channel_mut(0).copy_from_slice(&[1.0, 2.0]); 167 | buffer.channel_mut(1).copy_from_slice(&[3.0, 4.0]); 168 | buffer.channel_mut(2).copy_from_slice(&[5.0, 6.0]); 169 | 170 | buffer.resize(4); 171 | assert_eq!(buffer.channel(0), [1.0, 2.0, 0.0, 0.0]); 172 | assert_eq!(buffer.channel(1), [3.0, 4.0, 0.0, 0.0]); 173 | assert_eq!(buffer.channel(2), [5.0, 6.0, 0.0, 0.0]); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /plinth-core/src/collections.rs: -------------------------------------------------------------------------------- 1 | pub mod copy_from_slice; 2 | pub mod interleave_iterator; 3 | -------------------------------------------------------------------------------- /plinth-core/src/collections/copy_from_slice.rs: -------------------------------------------------------------------------------- 1 | pub trait CopyFromSlice { 2 | fn copy_from_slice_and_fill(&mut self, src: &[T], value: T); 3 | } 4 | 5 | impl CopyFromSlice for [T] { 6 | fn copy_from_slice_and_fill(&mut self, src: &[T], value: T) 7 | { 8 | let copy_len = usize::min(self.len(), src.len()); 9 | 10 | if copy_len > 0 { 11 | self[..copy_len].copy_from_slice(&src[..copy_len]); 12 | } 13 | if copy_len < self.len() { 14 | self[copy_len..].fill(value); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plinth-core/src/collections/interleave_iterator.rs: -------------------------------------------------------------------------------- 1 | use std::iter::zip; 2 | 3 | const MAX_ITERATORS: usize = 4; 4 | 5 | pub struct InterleaveIterator> { 6 | iterators: [Option; MAX_ITERATORS], 7 | iterator_count: usize, 8 | next_iterator_index: usize, 9 | } 10 | 11 | impl InterleaveIterator 12 | where 13 | I: Iterator 14 | { 15 | pub fn new>(iterators: II) -> InterleaveIterator { 16 | let mut iterator_array: [Option; MAX_ITERATORS] = Default::default(); 17 | 18 | let mut iterator_count = 0; 19 | for (it, array_it) in zip(iterators.into_iter(), iterator_array.iter_mut()) { 20 | *array_it = Some(it); 21 | iterator_count += 1; 22 | } 23 | 24 | InterleaveIterator { 25 | iterators: iterator_array, 26 | iterator_count, 27 | next_iterator_index: 0 28 | } 29 | } 30 | } 31 | 32 | impl> Iterator for InterleaveIterator { 33 | type Item = T; 34 | 35 | fn next(&mut self) -> Option { 36 | if self.iterator_count == 0 { 37 | return None; 38 | } 39 | 40 | let iterator = self.iterators[self.next_iterator_index].as_mut().unwrap(); 41 | let result = iterator.next(); 42 | 43 | self.next_iterator_index += 1; 44 | if self.next_iterator_index >= self.iterator_count { 45 | self.next_iterator_index = 0; 46 | } 47 | 48 | result 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::InterleaveIterator; 55 | 56 | #[test] 57 | fn empty() { 58 | let mut iterator = InterleaveIterator::new([[0u8; 0].iter()]); 59 | assert_eq!(iterator.next(), None); 60 | } 61 | 62 | #[test] 63 | fn iterate() { 64 | let a = [1.0, 2.0, 3.0]; 65 | let b = [4.0, 5.0, 6.0]; 66 | let c = [7.0, 8.0, 9.0]; 67 | 68 | let iterator = InterleaveIterator::new([a.iter(), b.iter(), c.iter()]); 69 | 70 | assert_eq!(iterator.copied().collect::>(), [1.0, 4.0, 7.0, 2.0, 5.0, 8.0, 3.0, 6.0, 9.0]); 71 | } 72 | 73 | #[test] 74 | fn iterate_mut() { 75 | let mut a = [1.0, 2.0, 3.0]; 76 | let mut b = [4.0, 5.0, 6.0]; 77 | let mut c = [7.0, 8.0, 9.0]; 78 | 79 | let iterator = InterleaveIterator::new([a.iter_mut(), b.iter_mut(), c.iter_mut()]); 80 | 81 | for (i, value) in iterator.enumerate() { 82 | if i % 2 == 0 { 83 | *value *= 10.0; 84 | } 85 | } 86 | 87 | assert_eq!(a, [10.0, 2.0, 30.0]); 88 | assert_eq!(b, [4.0, 50.0, 6.0]); 89 | assert_eq!(c, [70.0, 8.0, 90.0]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /plinth-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod buffers; 2 | pub mod collections; 3 | pub mod signals; 4 | pub mod util; 5 | -------------------------------------------------------------------------------- /plinth-core/src/signals.rs: -------------------------------------------------------------------------------- 1 | pub mod channels; 2 | pub mod frame; 3 | pub mod frame_iterator; 4 | pub mod frames_iterator; 5 | pub mod ptr_signal; 6 | pub mod signal; 7 | pub mod signal_base; 8 | pub mod signal_frame; 9 | pub mod slice; 10 | -------------------------------------------------------------------------------- /plinth-core/src/signals/channels.rs: -------------------------------------------------------------------------------- 1 | use super::signal::{Signal, SignalMut}; 2 | 3 | pub struct ChannelsIterator<'signal, S: Signal + ?Sized> { 4 | signal: &'signal S, 5 | channel_index: usize, 6 | } 7 | 8 | impl<'signal, S: Signal> ChannelsIterator<'signal, S> { 9 | pub fn new(signal: &'signal S) -> ChannelsIterator<'signal, S> { 10 | ChannelsIterator { 11 | signal, 12 | channel_index: 0, 13 | } 14 | } 15 | } 16 | 17 | impl Clone for ChannelsIterator<'_, S> { 18 | fn clone(&self) -> Self { 19 | Self { 20 | signal: self.signal, 21 | channel_index: 0, 22 | } 23 | } 24 | } 25 | 26 | impl<'signal, S: Signal + ?Sized> Iterator for ChannelsIterator<'signal, S> { 27 | type Item = &'signal [f32]; 28 | 29 | fn next(&mut self) -> Option { 30 | if self.channel_index < self.signal.channels() { 31 | let result = self.signal.channel(self.channel_index); 32 | self.channel_index += 1; 33 | Some(result) 34 | } else { 35 | None 36 | } 37 | } 38 | 39 | fn nth(&mut self, n: usize) -> Option { 40 | if n <= self.channel_index { 41 | return None 42 | } 43 | 44 | self.channel_index = n + 1; 45 | 46 | if n < self.signal.channels() { 47 | Some(self.signal.channel(n)) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | fn size_hint(&self) -> (usize, Option) { 54 | let size = self.signal.channels(); 55 | (size, Some(size)) 56 | } 57 | } 58 | 59 | pub struct ChannelsIteratorMut<'signal, S: SignalMut + ?Sized> { 60 | signal: &'signal mut S, 61 | channel_index: usize, 62 | } 63 | 64 | impl<'signal, S: SignalMut> ChannelsIteratorMut<'signal, S> { 65 | pub fn new(signal: &'signal mut S) -> ChannelsIteratorMut<'signal, S> { 66 | ChannelsIteratorMut { 67 | signal, 68 | channel_index: 0, 69 | } 70 | } 71 | } 72 | 73 | impl<'signal, S: SignalMut + ?Sized> Iterator for ChannelsIteratorMut<'signal, S> { 74 | type Item = &'signal mut [f32]; 75 | 76 | fn next(&mut self) -> Option { 77 | if self.channel_index < self.signal.channels() { 78 | let ptr = self.signal.channel_ptr_mut(self.channel_index); 79 | self.channel_index += 1; 80 | Some(unsafe { &mut *ptr }) 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | fn count(self) -> usize { 87 | self.signal.channels() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /plinth-core/src/signals/frame.rs: -------------------------------------------------------------------------------- 1 | use std::iter::zip; 2 | 3 | pub trait Frame<'frame> { 4 | type Iterator: Iterator; 5 | 6 | fn channels(&self) -> usize; 7 | fn channel(&self, index: usize) -> &f32; 8 | fn iter(&'frame self) -> Self::Iterator; 9 | } 10 | 11 | pub trait FrameMut<'frame> : Frame<'frame> { 12 | type IteratorMut: Iterator; 13 | 14 | fn channel_mut(&mut self, index: usize) -> &mut f32; 15 | fn iter_mut(&'frame mut self) -> Self::IteratorMut; 16 | 17 | fn copy_from<'source, I>(&'frame mut self, source: &'source impl Frame<'source, Iterator = I>) 18 | where 19 | I: Iterator, 20 | 'source: 'frame, 21 | { 22 | for (sample_self, sample_source) in zip(self.iter_mut(), source.iter()) { 23 | *sample_self = *sample_source; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plinth-core/src/signals/frame_iterator.rs: -------------------------------------------------------------------------------- 1 | use super::signal::{Signal, SignalMut}; 2 | 3 | pub struct FrameIterator<'signal, S: Signal> { 4 | signal: &'signal S, 5 | frame_index: usize, 6 | channel_index: usize, 7 | } 8 | 9 | impl FrameIterator<'_, S> { 10 | pub fn new(signal: &S, frame_index: usize) -> FrameIterator<'_, S> { 11 | FrameIterator { 12 | signal, 13 | frame_index, 14 | channel_index: 0, 15 | } 16 | } 17 | } 18 | 19 | impl<'signal, S: Signal> Iterator for FrameIterator<'signal, S> { 20 | type Item = &'signal f32; 21 | 22 | fn next(&mut self) -> Option { 23 | if self.channel_index >= self.signal.channels() { 24 | return None; 25 | } 26 | 27 | let result = self.signal.channel(self.channel_index).get(self.frame_index).unwrap(); 28 | self.channel_index += 1; 29 | 30 | Some(result) 31 | } 32 | } 33 | 34 | pub struct FrameIteratorMut<'signal, S: SignalMut> { 35 | signal: &'signal mut S, 36 | frame_index: usize, 37 | channel_index: usize, 38 | } 39 | 40 | impl FrameIteratorMut<'_, S> { 41 | pub fn new(signal: &mut S, frame_index: usize) -> FrameIteratorMut<'_, S> { 42 | FrameIteratorMut { 43 | signal, 44 | frame_index, 45 | channel_index: 0, 46 | } 47 | } 48 | } 49 | 50 | impl<'signal, S: SignalMut> Iterator for FrameIteratorMut<'signal, S> { 51 | type Item = &'signal mut f32; 52 | 53 | fn next(&mut self) -> Option { 54 | if self.channel_index >= self.signal.channels() { 55 | return None; 56 | } 57 | 58 | let ptr = self.signal.channel_ptr_mut(self.channel_index) as *mut f32; 59 | let ptr = unsafe { ptr.add(self.frame_index) }; 60 | let result = unsafe { &mut *ptr }; 61 | 62 | self.channel_index += 1; 63 | 64 | Some(result) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /plinth-core/src/signals/frames_iterator.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | 3 | use super::{signal::{Signal, SignalMut}, signal_frame::{SignalFrame, SignalFrameMut}}; 4 | 5 | pub struct FramesIterator<'signal, S: Signal + ?Sized> { 6 | signal: &'signal S, 7 | frame_index: usize, 8 | } 9 | 10 | impl FramesIterator<'_, S> { 11 | pub fn new(signal: &S) -> FramesIterator<'_, S> { 12 | FramesIterator { 13 | signal, 14 | frame_index: 0, 15 | } 16 | } 17 | } 18 | 19 | impl<'signal, S: Signal> Iterator for FramesIterator<'signal, S> { 20 | type Item = SignalFrame<'signal, S>; 21 | 22 | fn next(&mut self) -> Option { 23 | if self.frame_index >= self.signal.len() { 24 | return None; 25 | } 26 | 27 | let result = self.signal.frame(self.frame_index); 28 | self.frame_index += 1; 29 | 30 | Some(result) 31 | } 32 | } 33 | 34 | pub struct FramesIteratorMut<'signal, S: SignalMut + ?Sized> { 35 | signal: &'signal mut S, 36 | frame_index: usize, 37 | } 38 | 39 | impl FramesIteratorMut<'_, S> { 40 | pub fn new(signal: &mut S) -> FramesIteratorMut<'_, S> { 41 | FramesIteratorMut { 42 | signal, 43 | frame_index: 0, 44 | } 45 | } 46 | } 47 | 48 | impl<'signal, S: SignalMut> Iterator for FramesIteratorMut<'signal, S> { 49 | type Item = SignalFrameMut<'signal, S>; 50 | 51 | fn next(&mut self) -> Option { 52 | if self.frame_index >= self.signal.len() { 53 | return None; 54 | } 55 | 56 | let result = self.signal.frame_mut(self.frame_index); 57 | self.frame_index += 1; 58 | 59 | // Re-borrow to the correct lifetime, which is safe since self.signal has the same lifetime 60 | let result = unsafe { transmute::, SignalFrameMut<'_, S>>(result) }; 61 | Some(result) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plinth-core/src/signals/ptr_signal.rs: -------------------------------------------------------------------------------- 1 | use crate::util::ptr::any_null; 2 | 3 | use super::signal_base::{SignalBase, SignalMutBase}; 4 | 5 | pub struct PtrSignal { 6 | channels: usize, 7 | length: usize, 8 | channels_pointers: *const *const f32, 9 | } 10 | 11 | impl PtrSignal { 12 | /// # Safety 13 | /// 14 | /// Caller is responsible for channels and length matching the pointers, 15 | /// and for taking care the pointers live long enough 16 | pub unsafe fn from_pointers(channels: usize, length: usize, channels_pointers: *const *const f32) -> Self { 17 | assert!(!channels_pointers.is_null()); 18 | assert!(unsafe { !any_null(channels_pointers, channels) }); 19 | 20 | Self { 21 | channels, 22 | length, 23 | channels_pointers, 24 | } 25 | } 26 | 27 | pub fn pointers(&self) -> &[*const f32] { 28 | unsafe { std::slice::from_raw_parts(self.channels_pointers, self.channels) } 29 | } 30 | } 31 | 32 | impl SignalBase for PtrSignal { 33 | fn len(&self) -> usize { 34 | self.length 35 | } 36 | 37 | fn channels(&self) -> usize { 38 | self.channels 39 | } 40 | 41 | fn channel_ptr(&self, channel: usize) -> *const [f32] { 42 | unsafe { 43 | let channel_pointers = std::slice::from_raw_parts(self.channels_pointers, self.channels); 44 | let channel_pointer = std::slice::from_raw_parts(channel_pointers[channel], self.length); 45 | channel_pointer as _ 46 | } 47 | } 48 | } 49 | 50 | pub struct PtrSignalMut { 51 | channels: usize, 52 | length: usize, 53 | channels_pointers: *mut *mut f32, 54 | } 55 | 56 | impl PtrSignalMut { 57 | /// # Safety 58 | /// 59 | /// Caller is responsible for channels and length matching the pointers, 60 | /// and for taking care the pointers live long enough 61 | pub unsafe fn from_pointers(channels: usize, length: usize, channels_pointers: *mut *mut f32) -> Self { 62 | Self { 63 | channels, 64 | length, 65 | channels_pointers, 66 | } 67 | } 68 | 69 | pub fn pointers(&self) -> &[*mut f32] { 70 | unsafe { std::slice::from_raw_parts(self.channels_pointers, self.channels) } 71 | } 72 | } 73 | 74 | impl SignalBase for PtrSignalMut { 75 | fn len(&self) -> usize { 76 | self.length 77 | } 78 | 79 | fn channels(&self) -> usize { 80 | self.channels 81 | } 82 | 83 | fn channel_ptr(&self, channel: usize) -> *const [f32] { 84 | unsafe { 85 | let channel_pointers = std::slice::from_raw_parts(self.channels_pointers, self.channels); 86 | let channel_pointer = std::slice::from_raw_parts(channel_pointers[channel], self.length); 87 | channel_pointer as _ 88 | } 89 | } 90 | } 91 | 92 | impl SignalMutBase for PtrSignalMut { 93 | fn channel_ptr_mut(&mut self, channel: usize) -> *mut [f32] { 94 | unsafe { 95 | let channel_pointers = std::slice::from_raw_parts_mut(self.channels_pointers, self.channels); 96 | let channel_pointer = std::slice::from_raw_parts_mut(channel_pointers[channel], self.length); 97 | channel_pointer as _ 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /plinth-core/src/signals/signal.rs: -------------------------------------------------------------------------------- 1 | use std::{iter::zip, ops::{RangeBounds, Range}}; 2 | 3 | use itertools::izip; 4 | 5 | use crate::collections::{copy_from_slice::CopyFromSlice, interleave_iterator::InterleaveIterator}; 6 | 7 | use super::{channels::{ChannelsIterator, ChannelsIteratorMut}, frames_iterator::{FramesIterator, FramesIteratorMut}, signal_base::{SignalBase, SignalMutBase}, signal_frame::{SignalFrame, SignalFrameMut}, slice::{SignalSlice, SignalSliceMut}}; 8 | 9 | pub trait Signal : SignalBase { 10 | fn iter_channels(&self) -> ChannelsIterator<'_, Self>; 11 | fn frame(&self, index: usize) -> SignalFrame<'_, Self>; 12 | fn iter_frames(&self) -> FramesIterator<'_, Self>; 13 | 14 | fn channel(&self, channel: usize) -> &[f32] { 15 | unsafe { &*self.channel_ptr(channel) } 16 | } 17 | 18 | fn slice>(&self, range: T) -> SignalSlice<'_, Self> { 19 | SignalSlice::new(self, range) 20 | } 21 | 22 | fn iter_interleaved(&self) -> InterleaveIterator<&f32, std::slice::Iter<'_, f32>> { 23 | InterleaveIterator::new(self.iter_channels().map(|channel| channel.iter())) 24 | } 25 | 26 | fn mix_to(&self, self_gain: f32, other: &impl Signal, other_gain: f32, target: &mut impl SignalMut) { 27 | for (self_channel, other_channel, target_channel) in izip!(self.iter_channels(), other.iter_channels(), target.iter_channels_mut()) { 28 | for (self_sample, other_sample, target_sample) in izip!(self_channel, other_channel, target_channel) { 29 | *target_sample = self_sample * self_gain + other_sample * other_gain; 30 | } 31 | } 32 | } 33 | 34 | fn apply_wrap(&self, index: usize, length: usize, mut function: impl FnMut(&SignalSlice<'_, Self>, Range)) { 35 | assert!(index < self.len(), "index out of bounds: {index}"); 36 | 37 | let slice_len_1 = usize::min(length, self.len() - index); 38 | let slice_len_2 = length - slice_len_1; 39 | 40 | function(&self.slice(index..index + slice_len_1), 0..slice_len_1); 41 | if slice_len_2 > 0 { 42 | function(&self.slice(..slice_len_2), slice_len_1..slice_len_1 + slice_len_2); 43 | } 44 | } 45 | } 46 | 47 | pub trait SignalMut: Signal + SignalMutBase { 48 | fn iter_channels_mut(&mut self) -> ChannelsIteratorMut<'_, Self>; 49 | fn frame_mut(&mut self, index: usize) -> SignalFrameMut<'_, Self>; 50 | fn iter_frames_mut(&mut self) -> FramesIteratorMut<'_, Self>; 51 | 52 | fn channel_mut(&mut self, channel: usize) -> &mut [f32] { 53 | unsafe { &mut *self.channel_ptr_mut(channel) } 54 | } 55 | 56 | fn slice_mut>(&mut self, range: T) -> SignalSliceMut<'_, Self> { 57 | SignalSliceMut::new(self, range) 58 | } 59 | 60 | fn iter_interleaved_mut(&mut self) -> InterleaveIterator<&mut f32, std::slice::IterMut<'_, f32>> { 61 | InterleaveIterator::new(self.iter_channels_mut().map(|channel| channel.iter_mut())) 62 | } 63 | 64 | fn fill(&mut self, value: f32) { 65 | for channel in self.iter_channels_mut() { 66 | channel.fill(value); 67 | } 68 | } 69 | 70 | fn scale(&mut self, scale: f32) { 71 | for channel in self.iter_channels_mut() { 72 | for sample in channel.iter_mut() { 73 | *sample *= scale; 74 | } 75 | } 76 | } 77 | 78 | fn copy_from_signal(&mut self, source: &impl Signal) { 79 | assert_eq!(self.channels(), source.channels()); 80 | assert_eq!(self.len(), source.len(), "Attempting to copy a signal of length {} into a signal of length {}", source.len(), self.len()); 81 | 82 | for (target_channel, source_channel) in zip(self.iter_channels_mut(), source.iter_channels()) { 83 | target_channel.copy_from_slice(source_channel); 84 | } 85 | } 86 | 87 | fn copy_from_signal_and_fill(&mut self, source: &impl Signal, value: f32) { 88 | assert!(self.channels() == source.channels()); 89 | 90 | for (target_channel, source_channel) in zip(self.iter_channels_mut(), source.iter_channels()) { 91 | target_channel.copy_from_slice_and_fill(source_channel, value); 92 | } 93 | } 94 | 95 | fn add_from_signal(&mut self, source: &impl Signal) { 96 | assert!(self.channels() == source.channels()); 97 | 98 | for (target_channel, source_channel) in zip(self.iter_channels_mut(), source.iter_channels()) { 99 | for (target_sample, source_sample) in zip(target_channel, source_channel) { 100 | *target_sample += source_sample; 101 | } 102 | } 103 | } 104 | 105 | fn mix_signal(&mut self, gain_self: f32, source: &impl Signal, gain_source: f32) { 106 | for (self_channel, source_channel) in izip!(self.iter_channels_mut(), source.iter_channels()) { 107 | for (self_sample, source_sample) in zip(self_channel, source_channel) { 108 | *self_sample = *self_sample * gain_self + source_sample * gain_source; 109 | } 110 | } 111 | } 112 | 113 | fn apply_wrap_mut(&mut self, index: usize, length: usize, mut function: impl FnMut(&mut SignalSliceMut<'_, Self>, Range)) { 114 | assert!(index < self.len(), "index out of bounds: {index}/{}", self.len()); 115 | 116 | let slice_len_1 = usize::min(length, self.len() - index); 117 | let slice_len_2 = length - slice_len_1; 118 | 119 | function(&mut self.slice_mut(index..index + slice_len_1), 0..slice_len_1); 120 | if slice_len_2 > 0 { 121 | function(&mut self.slice_mut(..slice_len_2), slice_len_1..slice_len_1 + slice_len_2); 122 | } 123 | } 124 | } 125 | 126 | impl Signal for T { 127 | fn iter_channels(&self) -> ChannelsIterator<'_, Self> { 128 | ChannelsIterator::new(self) 129 | } 130 | 131 | fn frame(&self, index: usize) -> SignalFrame<'_, Self> { 132 | SignalFrame::new(self, index) 133 | } 134 | 135 | fn iter_frames(&self) -> FramesIterator<'_, Self> { 136 | FramesIterator::new(self) 137 | } 138 | } 139 | 140 | impl SignalMut for T { 141 | fn iter_channels_mut(&mut self) -> ChannelsIteratorMut<'_, Self> { 142 | ChannelsIteratorMut::new(self) 143 | } 144 | 145 | fn frame_mut(&mut self, index: usize) -> SignalFrameMut<'_, Self> { 146 | SignalFrameMut::new(self, index) 147 | } 148 | 149 | fn iter_frames_mut(&mut self) -> FramesIteratorMut<'_, Self> { 150 | FramesIteratorMut::new(self) 151 | } 152 | } 153 | 154 | impl> SignalBase for T { 155 | fn len(&self) -> usize { 156 | self.as_ref().len() 157 | } 158 | 159 | fn channels(&self) -> usize { 160 | 1 161 | } 162 | 163 | fn channel_ptr(&self, channel: usize) -> *const [f32] { 164 | assert_eq!(channel, 0); 165 | self.as_ref() as *const [f32] 166 | } 167 | } 168 | 169 | impl + AsRef<[f32]>> SignalMutBase for T { 170 | fn channel_ptr_mut(&mut self, channel: usize) -> *mut [f32] { 171 | assert_eq!(channel, 0); 172 | self.as_mut() as *mut [f32] 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /plinth-core/src/signals/signal_base.rs: -------------------------------------------------------------------------------- 1 | pub trait SignalBase { 2 | fn len(&self) -> usize; 3 | fn channels(&self) -> usize; 4 | fn channel_ptr(&self, channel: usize) -> *const [f32]; 5 | 6 | fn is_empty(&self) -> bool { 7 | self.len() == 0 8 | } 9 | } 10 | 11 | pub trait SignalMutBase { 12 | fn channel_ptr_mut(&mut self, channel: usize) -> *mut [f32]; 13 | } 14 | -------------------------------------------------------------------------------- /plinth-core/src/signals/signal_frame.rs: -------------------------------------------------------------------------------- 1 | use super::{frame::{Frame, FrameMut}, frame_iterator::{FrameIterator, FrameIteratorMut}, signal::{Signal, SignalMut}}; 2 | 3 | pub struct SignalFrame<'signal, S: Signal + ?Sized> { 4 | signal: &'signal S, 5 | frame_index: usize, 6 | } 7 | 8 | impl SignalFrame<'_, S> { 9 | pub fn new(signal: &S, frame_index: usize) -> SignalFrame<'_, S> { 10 | SignalFrame { 11 | signal, 12 | frame_index, 13 | } 14 | } 15 | } 16 | 17 | impl<'frame, S> Frame<'frame> for SignalFrame<'frame, S> 18 | where 19 | S: Signal, 20 | { 21 | type Iterator = FrameIterator<'frame, S>; 22 | 23 | fn channels(&self) -> usize { 24 | self.signal.channels() 25 | } 26 | 27 | fn channel(&self, index: usize) -> &f32 { 28 | &self.signal.channel(index)[self.frame_index] 29 | } 30 | 31 | fn iter(&self) -> FrameIterator<'frame, S> { 32 | FrameIterator::new(self.signal, self.frame_index) 33 | } 34 | } 35 | 36 | pub struct SignalFrameMut<'signal, S: SignalMut + ?Sized> { 37 | signal: &'signal mut S, 38 | frame_index: usize, 39 | } 40 | 41 | impl SignalFrameMut<'_, S> { 42 | pub fn new(signal: &mut S, frame_index: usize) -> SignalFrameMut<'_, S> { 43 | SignalFrameMut { 44 | signal, 45 | frame_index, 46 | } 47 | } 48 | } 49 | 50 | impl<'frame, S: SignalMut + 'frame> Frame<'frame> for SignalFrameMut<'_, S> { 51 | type Iterator = FrameIterator<'frame, S>; 52 | 53 | fn channels(&self) -> usize { 54 | self.signal.channels() 55 | } 56 | 57 | fn channel(&self, index: usize) -> &f32 { 58 | &self.signal.channel(index)[self.frame_index] 59 | } 60 | 61 | fn iter(&'frame self) -> FrameIterator<'frame, S> { 62 | FrameIterator::new(self.signal, self.frame_index) 63 | } 64 | } 65 | 66 | impl<'frame, S: SignalMut + 'frame> FrameMut<'frame> for SignalFrameMut<'_, S> { 67 | type IteratorMut = FrameIteratorMut<'frame, S>; 68 | 69 | fn channel_mut(&mut self, index: usize) -> &mut f32 { 70 | &mut self.signal.channel_mut(index)[self.frame_index] 71 | } 72 | 73 | fn iter_mut(&'frame mut self) -> FrameIteratorMut<'frame, S> { 74 | FrameIteratorMut::new(self.signal, self.frame_index) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /plinth-core/src/util.rs: -------------------------------------------------------------------------------- 1 | pub mod ptr; 2 | pub mod range; 3 | -------------------------------------------------------------------------------- /plinth-core/src/util/ptr.rs: -------------------------------------------------------------------------------- 1 | /// # Safety 2 | /// 3 | /// Caller is responsible that ptrs has at least `count` pointers 4 | pub unsafe fn any_null(ptrs: *const *const T, count: usize) -> bool { 5 | let slice = unsafe { std::slice::from_raw_parts(ptrs, count) }; 6 | slice.iter().any(|ptr| ptr.is_null()) 7 | } 8 | 9 | /// # Safety 10 | /// 11 | /// Caller is responsible that ptrs has at least `count` pointers 12 | pub unsafe fn any_null_mut(ptrs: *mut *mut T, count: usize) -> bool { 13 | let slice = unsafe { std::slice::from_raw_parts(ptrs, count) }; 14 | slice.iter().any(|ptr| ptr.is_null()) 15 | } 16 | -------------------------------------------------------------------------------- /plinth-core/src/util/range.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::{Range, RangeBounds, Bound, Add}, fmt::Display}; 2 | 3 | use num_traits::AsPrimitive; 4 | 5 | pub fn range_from_bounds(range: R, len: usize) -> Range 6 | where 7 | T: Add + Copy + Display + PartialOrd + 'static, 8 | R: RangeBounds, 9 | usize: AsPrimitive, 10 | { 11 | let (start, end) = range_from_bounds_impl(range, len); 12 | 13 | assert!(start <= len.as_(), "range start index {start} out of range of length {len}"); 14 | assert!(end <= len.as_(), "range end index {end} out of range of length {len}"); 15 | 16 | Range { start, end } 17 | } 18 | 19 | pub fn trimmed_range_from_bounds(range: R, len: usize) -> Range 20 | where 21 | T: Add + Copy + Display + PartialOrd + AsPrimitive + 'static, 22 | R: RangeBounds, 23 | usize: AsPrimitive, 24 | { 25 | let (mut start, mut end) = range_from_bounds_impl(range, len); 26 | 27 | start = usize::min(start.as_(), len).as_(); 28 | end = usize::min(end.as_(), len).as_(); 29 | 30 | Range { start, end } 31 | } 32 | 33 | 34 | pub fn range_from_bounds_impl(range: R, len: usize) -> (T, T) 35 | where 36 | T: Add + Copy + Display + PartialOrd + 'static, 37 | R: RangeBounds, 38 | usize: AsPrimitive, 39 | { 40 | let start = match range.start_bound() { 41 | Bound::Included(index) => *index, 42 | Bound::Excluded(index) => *index + 1.as_(), 43 | Bound::Unbounded => 0.as_(), 44 | }; 45 | 46 | let end = match range.end_bound() { 47 | Bound::Included(index) => *index + 1.as_(), 48 | Bound::Excluded(index) => *index, 49 | Bound::Unbounded => len.as_(), 50 | }; 51 | 52 | (start, end) 53 | } 54 | -------------------------------------------------------------------------------- /plinth-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plinth-derive" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | authors = ["Jussi Viiri "] 7 | readme = "README.md" 8 | repository = "https://github.com/ilmai/plugin-things" 9 | license = "MIT" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | plinth-plugin.workspace = true 16 | proc-macro2 = "1.0" 17 | quote = "1.0" 18 | syn = "2.0" 19 | -------------------------------------------------------------------------------- /plinth-derive/src/enums.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, Ident, LitStr, Meta, Expr, Lit}; 4 | 5 | pub fn generate_enum(input: DeriveInput) -> TokenStream { 6 | let enum_id = input.ident.clone(); 7 | let variants = parse_variants(&input); 8 | 9 | let variant_count = variants.len(); 10 | let fmt_cases = generate_fmt_cases(&variants); 11 | let from_usize_cases = generate_from_usize_cases(&variants); 12 | let from_string_cases = generate_from_string_cases(&variants); 13 | let to_usize_cases = generate_to_usize_cases(&variants); 14 | let to_string_cases = generate_to_string_cases(&variants); 15 | 16 | quote! { 17 | impl std::fmt::Display for #enum_id { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | let name = match self { 20 | #(#fmt_cases)* 21 | }; 22 | 23 | write!(f, "{name}") 24 | } 25 | } 26 | 27 | impl ::plinth_plugin::Enum for #enum_id { 28 | const COUNT: usize = #variant_count; 29 | 30 | fn from_usize(value: usize) -> Option { 31 | match value { 32 | #(#from_usize_cases)* 33 | _ => None 34 | } 35 | } 36 | 37 | fn from_string(string: &str) -> Option { 38 | match string { 39 | #(#from_string_cases)* 40 | _ => None 41 | } 42 | } 43 | 44 | fn to_usize(&self) -> usize { 45 | match self { 46 | #(#to_usize_cases)* 47 | } 48 | } 49 | 50 | fn to_string(&self) -> String { 51 | match self { 52 | #(#to_string_cases)* 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct Variant { 60 | id: Ident, 61 | name: Option, 62 | } 63 | 64 | fn parse_variants(input: &DeriveInput) -> Vec { 65 | let syn::Data::Enum(ref body) = input.data else { 66 | panic!("Macro can only be used on enums"); 67 | }; 68 | 69 | body.variants.iter() 70 | .map(|variant| { 71 | if !variant.fields.is_empty() { 72 | panic!("Macro can only be used on enums that doesn't contain fields"); 73 | } 74 | 75 | let mut name = None; 76 | 77 | for attr in variant.attrs.iter() { 78 | if attr.path().is_ident("name") { 79 | let Meta::NameValue(name_value) = &attr.meta else { 80 | panic!("Name syntax error"); 81 | }; 82 | 83 | let Expr::Lit(lit) = &name_value.value else { 84 | panic!("Name syntax error"); 85 | }; 86 | 87 | let Lit::Str(str_lit) = &lit.lit else { 88 | panic!("Name syntax error"); 89 | }; 90 | 91 | name = Some(str_lit.clone()); 92 | } 93 | } 94 | 95 | Variant { 96 | id: variant.ident.clone(), 97 | name, 98 | } 99 | }) 100 | .collect() 101 | } 102 | 103 | fn generate_fmt_cases(variants: &[Variant]) -> Vec { 104 | variants.iter().map(|variant| { 105 | let id = &variant.id; 106 | let name = variant.name 107 | .as_ref() 108 | .map(|name| name.value()) 109 | .unwrap_or(variant.id.to_string()); 110 | 111 | quote! { 112 | Self::#id => #name, 113 | } 114 | }).collect() 115 | } 116 | 117 | fn generate_from_usize_cases(variants: &[Variant]) -> Vec { 118 | variants.iter().enumerate().map(|(index, variant)| { 119 | let id = &variant.id; 120 | 121 | quote! { 122 | #index => Some(Self::#id), 123 | } 124 | }).collect() 125 | } 126 | 127 | fn generate_from_string_cases(variants: &[Variant]) -> Vec { 128 | variants.iter().map(|variant| { 129 | let id = &variant.id; 130 | let id_string = id.to_string(); 131 | let name = variant.name.clone().unwrap_or_else(|| LitStr::new(&id_string, variant.id.span())); 132 | 133 | quote! { 134 | #name => Some(Self::#id), 135 | } 136 | }).collect() 137 | } 138 | 139 | fn generate_to_usize_cases(variants: &[Variant]) -> Vec { 140 | variants.iter().enumerate().map(|(index, variant)| { 141 | let id = &variant.id; 142 | 143 | quote! { 144 | Self::#id => #index, 145 | } 146 | }).collect() 147 | } 148 | 149 | fn generate_to_string_cases(variants: &[Variant]) -> Vec { 150 | variants.iter().map(|variant| { 151 | let id = &variant.id; 152 | let id_string = id.to_string(); 153 | let name = variant.name.clone().unwrap_or_else(|| LitStr::new(&id_string, variant.id.span())); 154 | 155 | quote! { 156 | Self::#id => #name.to_string(), 157 | } 158 | }).collect() 159 | } 160 | -------------------------------------------------------------------------------- /plinth-derive/src/kind.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, Ident}; 4 | 5 | const ID_MASK: u32 = i32::MAX as u32; 6 | 7 | pub fn generate_parameter_kind(input: DeriveInput) -> TokenStream { 8 | let enum_id = input.ident.clone(); 9 | let variants = parse_variants(&input); 10 | 11 | let match_cases = generate_match_cases(enum_id.clone(), &variants); 12 | 13 | quote! { 14 | impl ::plinth_plugin::parameters::kind::ParameterKind for #enum_id { 15 | } 16 | 17 | impl Into<::plinth_plugin::ParameterId> for #enum_id { 18 | fn into(self) -> ::plinth_plugin::ParameterId { 19 | match self { 20 | #(#match_cases)* 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | struct Variant { 28 | id: Ident, 29 | fields: Vec, 30 | } 31 | 32 | 33 | fn parse_variants(input: &DeriveInput) -> Vec { 34 | let syn::Data::Enum(ref body) = input.data else { 35 | panic!("Macro can only be used on enums"); 36 | }; 37 | 38 | body.variants.iter() 39 | .map(|variant| { 40 | let fields = variant.fields.iter() 41 | .map(|field| { 42 | field.ident.clone().expect("Macro can't be used on tuple enums") 43 | }) 44 | .collect(); 45 | 46 | Variant { 47 | id: variant.ident.clone(), 48 | fields, 49 | } 50 | }) 51 | .collect() 52 | } 53 | 54 | fn generate_match_cases(enum_id: Ident, variants: &[Variant]) -> Vec { 55 | variants.iter().enumerate().map(|(index, variant)| { 56 | let variant_id = &variant.id; 57 | let fields = &variant.fields; 58 | 59 | if fields.is_empty() { 60 | quote! { 61 | #enum_id::#variant_id => { 62 | ::plinth_plugin::xxhash_rust::xxh32::xxh32(&#index.to_le_bytes(), 0) & #ID_MASK 63 | } 64 | } 65 | } else { 66 | let field_hashes: Vec<_> = fields.iter() 67 | .map(|field_id| { 68 | quote! { 69 | let hash = ::plinth_plugin::xxhash_rust::xxh32::xxh32(&#field_id.to_le_bytes(), hash); 70 | } 71 | }) 72 | .collect(); 73 | 74 | quote! { 75 | #enum_id::#variant_id { #(#fields),* } => { 76 | let hash = ::plinth_plugin::xxhash_rust::xxh32::xxh32(&#index.to_le_bytes(), 0); 77 | #(#field_hashes)* 78 | hash & #ID_MASK 79 | } 80 | } 81 | } 82 | }).collect() 83 | } 84 | -------------------------------------------------------------------------------- /plinth-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod enums; 2 | mod kind; 3 | 4 | use enums::generate_enum; 5 | use kind::generate_parameter_kind; 6 | use proc_macro::TokenStream; 7 | use syn::parse_macro_input; 8 | 9 | #[proc_macro_derive(Enum, attributes(name))] 10 | pub fn derive_enum(input: TokenStream) -> TokenStream { 11 | let input = parse_macro_input!(input); 12 | let output = generate_enum(input); 13 | output.into() 14 | } 15 | 16 | #[proc_macro_derive(ParameterKind)] 17 | pub fn derive_parameter_kind(input: TokenStream) -> TokenStream { 18 | let input = parse_macro_input!(input); 19 | let output = generate_parameter_kind(input); 20 | output.into() 21 | } 22 | -------------------------------------------------------------------------------- /plinth-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plinth-plugin" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | authors = ["Jussi Viiri "] 7 | readme = "README.md" 8 | repository = "https://github.com/ilmai/plugin-things" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | atomic_refcell = "0.1" 13 | clap-sys = "0.5" 14 | log.workspace = true 15 | num-derive = "0.4" 16 | num-traits.workspace = true 17 | plinth-core.workspace = true 18 | portable-atomic.workspace = true 19 | raw-window-handle = "0.6" 20 | rtrb = "0.3" 21 | thiserror = "2.0" 22 | vst3 = "0.1" 23 | widestring = "1.1" 24 | xxhash-rust = { version = "0.8", features = ["xxh3", "xxh32"] } 25 | 26 | [build-dependencies] 27 | bindgen = "0.71" 28 | 29 | [dev-dependencies] 30 | approx = "0.5" 31 | -------------------------------------------------------------------------------- /plinth-plugin/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | #[cfg(target_os="macos")] 6 | generate_plinth_bindings(); 7 | } 8 | 9 | #[allow(dead_code)] 10 | fn generate_plinth_bindings() { 11 | let bindings = bindgen::Builder::default() 12 | .header("include/plinth_auv3.h") 13 | .blocklist_type("AURenderEvent") 14 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 15 | .generate() 16 | .expect("Unable to generate bindings"); 17 | 18 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 19 | 20 | bindings 21 | .write_to_file(out_path.join("bindings.rs")) 22 | .expect("Couldn't write bindings!"); 23 | } 24 | -------------------------------------------------------------------------------- /plinth-plugin/include/plinth_auv3.h: -------------------------------------------------------------------------------- 1 | #ifndef FILTER_PLUGIN_H 2 | #define FILTER_PLUGIN_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | const size_t PLINTH_AUV3_MAX_STRING_LENGTH = 100; 13 | 14 | struct ParameterInfo { 15 | char* identifier; 16 | char* name; 17 | int64_t parentGroupIndex; 18 | uint64_t address; 19 | uint64_t steps; 20 | }; 21 | 22 | struct ParameterGroupInfo { 23 | char* identifier; 24 | char* name; 25 | int64_t parentGroupIndex; 26 | }; 27 | 28 | union AURenderEvent; 29 | 30 | void* plinth_auv3_create(); 31 | void plinth_auv3_destroy(void* wrapper); 32 | 33 | void plinth_auv3_activate(void* wrapper, double sampleRate, uint64_t maxBlockSize); 34 | void plinth_auv3_deactivate(void* wrapper); 35 | 36 | bool plinth_auv3_has_aux_bus(); 37 | double plinth_auv3_tail_length(void* wrapper); 38 | 39 | void plinth_auv3_process( 40 | void* wrapper, 41 | const float** input, 42 | const float** aux, 43 | float **output, 44 | uint32_t channels, 45 | uint32_t frames, 46 | bool playing, 47 | double tempo, 48 | int64_t positionSamples, 49 | const union AURenderEvent *firstEvent 50 | ); 51 | 52 | // Parameter interface 53 | size_t plinth_auv3_group_count(void* wrapper); 54 | void plinth_auv3_group_info(void* wrapper, size_t index, struct ParameterGroupInfo* info); 55 | 56 | size_t plinth_auv3_parameter_count(void* wrapper); 57 | void plinth_auv3_parameter_info(void* wrapper, size_t index, struct ParameterInfo* info); 58 | 59 | float plinth_auv3_get_parameter_value(void* wrapper, uint64_t address); 60 | void plinth_auv3_set_parameter_value(void* wrapper, uint64_t address, float value); 61 | 62 | void plinth_auv3_parameter_normalized_to_string(void* wrapper, uint64_t address, float value, char* string); 63 | 64 | // State interface 65 | void plinth_auv3_load_state(void* wrapper, void* context, size_t (*read)(void*, uint8_t*, size_t)); 66 | void plinth_auv3_save_state(void* wrapper, void* context, size_t (*write)(void*, const uint8_t*, size_t)); 67 | 68 | // Editor interface 69 | void plinth_auv3_editor_create( 70 | void* wrapper, 71 | void* editor_context, 72 | void (*start_parameter_change)(void*, uint32_t), 73 | void (*change_parameter_value)(void*, uint32_t, float), 74 | void (*end_parameter_change)(void*, uint32_t) 75 | ); 76 | void plinth_auv3_editor_get_default_size(double* width, double* height); 77 | void plinth_auv3_editor_get_size(void* wrapper, double* width, double* height); 78 | void plinth_auv3_editor_set_size(void* wrapper, double width, double height); 79 | void plinth_auv3_editor_open(void* wrapper, void* parent); 80 | void plinth_auv3_editor_close(void* wrapper); 81 | 82 | #ifdef __cplusplus 83 | } 84 | #endif 85 | 86 | #endif // FILTER_PLUGIN_H 87 | -------------------------------------------------------------------------------- /plinth-plugin/src/editor.rs: -------------------------------------------------------------------------------- 1 | use raw_window_handle::RawWindowHandle; 2 | 3 | #[cfg(target_os="linux")] 4 | pub(crate) const FRAME_TIMER_MILLISECONDS: u64 = 16; 5 | 6 | pub trait Editor { 7 | const DEFAULT_SIZE: (f64, f64); 8 | 9 | fn open(&mut self, parent: RawWindowHandle); 10 | fn close(&mut self); 11 | 12 | /// Returns current window size 13 | fn window_size(&self) -> (f64, f64) { 14 | Self::DEFAULT_SIZE 15 | } 16 | 17 | fn can_resize(&self) -> bool { 18 | false 19 | } 20 | 21 | /// Called by the host to see if a window size is supported 22 | /// Return Some with a supported size if resizing based on the incoming size is supported 23 | /// Otherwise, return None 24 | fn check_window_size(&self, _size: (f64, f64)) -> Option<(f64, f64)> { 25 | None 26 | } 27 | 28 | /// Set new window size; should only be called when window is created and after a previous call to check_window_size() 29 | fn set_window_size(&mut self, _width: f64, _height: f64) {} 30 | 31 | /// Set window scale; this is a suggestion that can be ignored, but it's probably a good default scale for the plugin based on OS DPI 32 | fn set_scale(&mut self, _scale: f64) {} 33 | 34 | fn on_frame(&mut self); 35 | } 36 | 37 | pub struct NoEditor; 38 | 39 | impl Editor for NoEditor { 40 | const DEFAULT_SIZE: (f64, f64) = (0.0, 0.0); 41 | 42 | fn open(&mut self, _parent: RawWindowHandle) {} 43 | fn close(&mut self) {} 44 | 45 | fn on_frame(&mut self) {} 46 | } 47 | -------------------------------------------------------------------------------- /plinth-plugin/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | ParameterIdError, 4 | ParameterRangeError, 5 | SerializationError, 6 | IoError(std::io::Error), 7 | } 8 | 9 | impl From for Error { 10 | fn from(error: std::io::Error) -> Self { 11 | Self::IoError(error) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plinth-plugin/src/event.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | 4 | use plinth_core::signals::{signal::SignalMut, slice::SignalSliceMut}; 5 | 6 | use crate::parameters::ParameterId; 7 | 8 | #[derive(Clone, Debug)] 9 | #[non_exhaustive] 10 | pub enum Event { 11 | // Note events 12 | NoteOn { 13 | channel: i16, 14 | key: i16, 15 | note: i32, 16 | velocity: f64, 17 | }, 18 | 19 | NoteOff { 20 | channel: i16, 21 | key: i16, 22 | note: i32, 23 | velocity: f64, 24 | }, 25 | 26 | PitchBend { 27 | channel: i16, 28 | key: i16, 29 | note: i32, 30 | semitones: f64, 31 | }, 32 | 33 | // Parameter events 34 | StartParameterChange { 35 | id: ParameterId, 36 | }, 37 | 38 | EndParameterChange { 39 | id: ParameterId, 40 | }, 41 | 42 | ParameterValue { 43 | sample_offset: usize, 44 | id: ParameterId, 45 | value: f64, 46 | }, 47 | 48 | ParameterModulation { 49 | sample_offset: usize, 50 | id: ParameterId, 51 | amount: f64, 52 | }, 53 | } 54 | 55 | impl Event { 56 | pub fn split_signal_at_events(signal: &mut S, events: I) -> SignalSplitter<'_, I, S> 57 | where 58 | I: Iterator, 59 | S: SignalMut, 60 | { 61 | SignalSplitter::new(signal, events) 62 | } 63 | 64 | pub fn sample_offset(&self) -> usize { 65 | match self { 66 | Event::ParameterValue { sample_offset, .. } => *sample_offset, 67 | Event::ParameterModulation { sample_offset, .. } => *sample_offset, 68 | 69 | _ => 0 70 | } 71 | } 72 | } 73 | 74 | pub struct SignalSplitter<'signal, I, S> 75 | where 76 | I: Iterator, 77 | S: SignalMut, 78 | { 79 | signal: *mut S, 80 | events: I, 81 | offset: usize, 82 | 83 | _phantom_lifetime: PhantomData<&'signal S>, 84 | } 85 | 86 | impl<'signal, I, S> SignalSplitter<'signal, I, S> 87 | where 88 | I: Iterator, 89 | S: SignalMut, 90 | { 91 | pub fn new(signal: &'signal mut S, events: I) -> Self { 92 | Self { 93 | signal, 94 | events, 95 | offset: 0, 96 | 97 | _phantom_lifetime: PhantomData, 98 | } 99 | } 100 | } 101 | 102 | impl<'signal, I, S> Iterator for SignalSplitter<'signal, I, S> 103 | where 104 | I: Iterator, 105 | S: SignalMut, 106 | { 107 | type Item = (SignalSliceMut<'signal, S>, Option); 108 | 109 | fn next(&mut self) -> Option { 110 | let signal = unsafe { &mut *self.signal }; 111 | 112 | loop { 113 | let Some(next_event) = self.events.next() else { 114 | if self.offset < signal.len() { 115 | let signal_len = signal.len(); 116 | let signal_slice = signal.slice_mut(self.offset..); 117 | self.offset = signal_len; 118 | 119 | return Some((signal_slice, None)); 120 | } else { 121 | return None; 122 | } 123 | }; 124 | 125 | match next_event { 126 | Event::ParameterValue { sample_offset, .. } | 127 | Event::ParameterModulation { sample_offset, .. } => { 128 | let sample_offset = usize::min(sample_offset, signal.len()); 129 | 130 | let result = (signal.slice_mut(self.offset..sample_offset), Some(next_event)); 131 | self.offset = sample_offset; 132 | return Some(result); 133 | }, 134 | 135 | _ => { continue; }, 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os="macos")] 2 | pub mod auv3; 3 | pub mod clap; 4 | pub mod vst3; 5 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | #![allow(non_upper_case_globals)] 4 | 5 | mod au_render_event; 6 | mod event; 7 | mod host; 8 | mod macros; 9 | mod parameters; 10 | mod plugin; 11 | mod reader; 12 | mod util; 13 | mod wrapper; 14 | mod writer; 15 | 16 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 17 | 18 | pub use au_render_event::AURenderEvent; 19 | pub use event::EventIterator; 20 | pub use host::Auv3Host; 21 | pub use plugin::Auv3Plugin; 22 | pub use reader::Auv3Reader; 23 | pub use util::parameter_multiplier; 24 | pub use wrapper::Auv3Wrapper; 25 | pub use writer::Auv3Writer; 26 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/au_render_event.rs: -------------------------------------------------------------------------------- 1 | use std::mem::ManuallyDrop; 2 | 3 | #[repr(u8)] 4 | #[derive(Clone, Copy, Debug)] 5 | #[allow(dead_code)] 6 | pub enum AURenderEventType { 7 | AURenderEventParameter = 1, 8 | AURenderEventParameterRamp = 2, 9 | AURenderEventMIDI = 8, 10 | AURenderEventMIDISysEx = 9, 11 | AURenderEventMIDIEventList = 10 12 | } 13 | 14 | #[repr(C)] 15 | pub struct AURenderEventHeader { 16 | pub next: *mut AURenderEvent, 17 | pub event_sample_time: i64, 18 | pub event_type: AURenderEventType, 19 | _reserved: u8, 20 | } 21 | 22 | #[repr(C)] 23 | pub struct AUParameterEvent { 24 | pub next: *mut AURenderEvent, 25 | pub event_sample_time: i64, 26 | pub event_type: AURenderEventType, 27 | pub _reserved: [u8; 3], 28 | pub ramp_duration_sample_frames: u32, 29 | pub parameter_address: u64, 30 | pub value: f32, 31 | } 32 | 33 | #[repr(C)] 34 | pub struct AUMIDIEvent { 35 | pub next: *mut AURenderEvent, 36 | pub event_sample_time: i64, 37 | pub event_type: AURenderEventType, 38 | _reserved: u8, 39 | pub length: u16, 40 | pub cable: u8, 41 | pub data: [u8; 3], 42 | } 43 | 44 | #[repr(C)] 45 | pub struct AUMIDIEventList { 46 | pub next: *mut AURenderEvent, 47 | pub event_sample_time: i64, 48 | pub event_type: AURenderEventType, 49 | _reserved: u8, 50 | pub cable: u8, 51 | pub event_list: MIDIEventList, 52 | } 53 | 54 | #[repr(C)] 55 | pub union AURenderEvent { 56 | pub header: ManuallyDrop, 57 | pub parameter: ManuallyDrop, 58 | pub midi: ManuallyDrop, 59 | pub midi_events_list: ManuallyDrop, 60 | } 61 | 62 | #[repr(i32)] 63 | #[allow(dead_code)] 64 | pub enum MIDIProtocolId { 65 | Protocol1_0 = 1, 66 | Protocol2_0 = 2, 67 | } 68 | 69 | #[repr(C)] 70 | pub struct MIDIEventPacket { 71 | pub time_stamp: u64, 72 | pub word_count: u32, 73 | pub words: [u32; 64], 74 | } 75 | 76 | #[repr(C)] 77 | pub struct MIDIEventList { 78 | pub protocol: MIDIProtocolId, 79 | pub num_packets: u32, 80 | pub packet: [MIDIEventPacket; 1], 81 | } 82 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/event.rs: -------------------------------------------------------------------------------- 1 | use crate::{Event, ParameterId}; 2 | 3 | use super::au_render_event::{AURenderEvent, AURenderEventType}; 4 | 5 | pub struct EventIterator<'ids> { 6 | next_event: *const AURenderEvent, 7 | parameter_ids: &'ids [ParameterId], 8 | } 9 | 10 | impl<'ids> EventIterator<'ids> { 11 | pub fn new(first_event: *const AURenderEvent, parameter_ids: &'ids [ParameterId]) -> Self { 12 | Self { 13 | next_event: first_event, 14 | parameter_ids, 15 | } 16 | } 17 | } 18 | 19 | impl Iterator for EventIterator<'_> { 20 | type Item = Event; 21 | 22 | fn next(&mut self) -> Option { 23 | while !self.next_event.is_null() { 24 | let next_event = unsafe { &*self.next_event }; 25 | let header = unsafe { &next_event.header }; 26 | 27 | self.next_event = header.next; 28 | 29 | match header.event_type { 30 | AURenderEventType::AURenderEventParameter | AURenderEventType::AURenderEventParameterRamp => { 31 | let parameter_event = unsafe { &next_event.parameter }; 32 | 33 | // auval will use invalid parameter addresses when testing for parameter ramping so 34 | // just ignore those events 35 | if !self.parameter_ids.contains(&(parameter_event.parameter_address as _)) { 36 | continue; 37 | } 38 | 39 | // We don't deal too well with time travel so restrict the range 40 | // For example auval will send events with some wild values here 41 | let sample_offset = i64::max(0, parameter_event.event_sample_time); 42 | 43 | return Some(Event::ParameterValue { 44 | sample_offset: sample_offset as _, 45 | id: parameter_event.parameter_address as _, 46 | value: parameter_event.value as _, 47 | }); 48 | }, 49 | 50 | _ => {} 51 | } 52 | } 53 | 54 | None 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/host.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, ffi::c_void, sync::{atomic::{AtomicBool, Ordering}, Arc, Mutex}}; 2 | 3 | use crate::{Host, ParameterId, ParameterValue}; 4 | 5 | use super::{parameter_multiplier, parameters::CachedParameter}; 6 | 7 | pub struct Auv3Host { 8 | context: *mut c_void, 9 | start_parameter_change: unsafe extern "C-unwind" fn(*mut c_void, u32), 10 | change_parameter_value: unsafe extern "C-unwind" fn(*mut c_void, u32, f32), 11 | end_parameter_change: unsafe extern "C-unwind" fn(*mut c_void, u32), 12 | 13 | sending_parameter_change_from_editor: Arc, 14 | parameter_index_from_id: HashMap, 15 | cached_parameters: Arc>>, 16 | } 17 | 18 | impl Auv3Host { 19 | pub(super) fn new( 20 | editor_context: *mut c_void, 21 | start_parameter_change: unsafe extern "C-unwind" fn(*mut c_void, u32), 22 | change_parameter_value: unsafe extern "C-unwind" fn(*mut c_void, u32, f32), 23 | end_parameter_change: unsafe extern "C-unwind" fn(*mut c_void, u32), 24 | sending_parameter_change_from_editor: Arc, 25 | cached_parameters: Arc>>, 26 | parameter_index_from_id: HashMap, 27 | ) -> Self 28 | { 29 | Self { 30 | context: editor_context, 31 | start_parameter_change, 32 | end_parameter_change, 33 | change_parameter_value, 34 | 35 | sending_parameter_change_from_editor, 36 | parameter_index_from_id, 37 | cached_parameters, 38 | } 39 | } 40 | } 41 | 42 | impl Host for Auv3Host { 43 | fn can_resize(&self) -> bool { 44 | false 45 | } 46 | 47 | fn resize_view(&self, _width: f64, _height: f64) -> bool { 48 | false 49 | } 50 | 51 | fn start_parameter_change(&self, id: ParameterId) { 52 | self.sending_parameter_change_from_editor.store(true, Ordering::Release); 53 | unsafe { (self.start_parameter_change)(self.context, id); } 54 | self.sending_parameter_change_from_editor.store(false, Ordering::Release); 55 | } 56 | 57 | fn change_parameter_value(&self, id: ParameterId, normalized: ParameterValue) { 58 | self.sending_parameter_change_from_editor.store(true, Ordering::Release); 59 | unsafe { (self.change_parameter_value)(self.context, id, normalized as _); } 60 | self.sending_parameter_change_from_editor.store(false, Ordering::Release); 61 | 62 | // Update cached value 63 | let index = self.parameter_index_from_id.get(&id).unwrap(); 64 | let mut cached_parameters = self.cached_parameters.lock().unwrap(); 65 | let parameter = cached_parameters.get_mut(*index).unwrap(); 66 | 67 | parameter.value = (normalized * parameter_multiplier(¶meter.info)) as _; 68 | } 69 | 70 | fn end_parameter_change(&self, id: ParameterId) { 71 | self.sending_parameter_change_from_editor.store(true, Ordering::Release); 72 | unsafe { (self.end_parameter_change)(self.context, id); } 73 | self.sending_parameter_change_from_editor.store(false, Ordering::Release); 74 | } 75 | 76 | fn mark_state_dirty(&self) { 77 | // TODO 78 | } 79 | } 80 | 81 | unsafe impl Send for Auv3Host {} 82 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/parameters.rs: -------------------------------------------------------------------------------- 1 | use crate::{parameters::info::ParameterInfo, ParameterId}; 2 | 3 | pub(super) struct CachedParameter { 4 | pub id: ParameterId, 5 | pub info: ParameterInfo, 6 | pub value: f32, 7 | } 8 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::Plugin; 2 | 3 | pub trait Auv3Plugin : Plugin { 4 | const AUV3_ID: &'static str; 5 | } 6 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/reader.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | pub struct Auv3Reader { 4 | context: *mut c_void, 5 | read: unsafe extern "C-unwind" fn(*mut ::std::ffi::c_void, *mut u8, usize) -> usize, 6 | } 7 | 8 | impl Auv3Reader { 9 | pub fn new( 10 | context: *mut c_void, 11 | read: unsafe extern "C-unwind" fn(*mut ::std::ffi::c_void, *mut u8, usize) -> usize, 12 | ) -> Self { 13 | Self { 14 | context, 15 | read, 16 | } 17 | } 18 | } 19 | 20 | impl std::io::Read for Auv3Reader { 21 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 22 | let bytes_read = unsafe { (self.read)(self.context, buf.as_mut_ptr(), buf.len()) }; 23 | Ok(bytes_read) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/util.rs: -------------------------------------------------------------------------------- 1 | use crate::parameters::info::ParameterInfo; 2 | 3 | pub fn parameter_multiplier(info: &ParameterInfo) -> f64 { 4 | if info.steps() > 0 { 5 | info.steps() as f64 6 | } else { 7 | 1.0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/auv3/writer.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | pub struct Auv3Writer { 4 | context: *mut c_void, 5 | write: unsafe extern "C-unwind" fn(*mut ::std::ffi::c_void, *const u8, usize) -> usize, 6 | } 7 | 8 | impl Auv3Writer { 9 | pub fn new( 10 | context: *mut c_void, 11 | write: unsafe extern "C-unwind" fn(*mut ::std::ffi::c_void, *const u8, usize) -> usize, 12 | ) -> Self { 13 | Self { 14 | context, 15 | write, 16 | } 17 | } 18 | } 19 | 20 | impl std::io::Write for Auv3Writer { 21 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 22 | let bytes_written = unsafe { (self.write)(self.context, buf.as_ptr(), buf.len()) }; 23 | Ok(bytes_written) 24 | } 25 | 26 | fn flush(&mut self) -> std::io::Result<()> { 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap.rs: -------------------------------------------------------------------------------- 1 | mod descriptor; 2 | mod entry_point; 3 | mod event; 4 | mod extensions; 5 | mod factory; 6 | mod features; 7 | mod host; 8 | mod macros; 9 | mod parameters; 10 | mod plugin; 11 | mod plugin_instance; 12 | mod stream; 13 | mod transport; 14 | 15 | pub use entry_point::EntryPoint; 16 | pub use factory::Factory; 17 | pub use features::Feature; 18 | pub use plugin::ClapPlugin; 19 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/descriptor.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::{c_char, CStr, CString}, ptr::null}; 2 | 3 | use clap_sys::{plugin::clap_plugin_descriptor, version::CLAP_VERSION}; 4 | 5 | use super::plugin::ClapPlugin; 6 | 7 | struct DescriptorData { 8 | id: CString, 9 | name: CString, 10 | vendor: CString, 11 | version: CString, 12 | 13 | features: Vec, 14 | feature_pointers: Vec<*const c_char>, 15 | 16 | url: CString, 17 | manual_url: CString, 18 | support_url: CString, 19 | description: CString, 20 | } 21 | 22 | // SAFETY: feature_pointers is never modified after creation 23 | unsafe impl Send for DescriptorData {} 24 | 25 | pub struct Descriptor { 26 | data: Box, 27 | raw: clap_plugin_descriptor, 28 | } 29 | 30 | impl Descriptor { 31 | pub fn new() -> Self { 32 | let mut data = Box::new(DescriptorData { 33 | id: CString::new(P::CLAP_ID).unwrap(), 34 | name: CString::new(P::NAME).unwrap(), 35 | vendor: CString::new(P::VENDOR).unwrap(), 36 | version: CString::new(P::VERSION).unwrap(), 37 | 38 | features: P::FEATURES.iter().map(|feature| CString::new(feature.to_str()).unwrap()).collect(), 39 | feature_pointers: Vec::new(), 40 | 41 | url: CString::new(P::URL.unwrap_or_default()).unwrap(), 42 | manual_url: CString::new(P::MANUAL_URL.unwrap_or_default()).unwrap(), 43 | support_url: CString::new(P::SUPPORT_URL.unwrap_or_default()).unwrap(), 44 | description: CString::new(P::DESCRIPTION.unwrap_or_default()).unwrap(), 45 | }); 46 | 47 | // Save feature string pointers 48 | data.feature_pointers = data.features 49 | .iter() 50 | .map(|feature| feature.as_ptr()) 51 | .collect(); 52 | data.feature_pointers.push(null()); // Null terminator 53 | 54 | let raw = clap_plugin_descriptor { 55 | clap_version: CLAP_VERSION, 56 | id: data.id.as_ptr(), 57 | name: data.name.as_ptr(), 58 | vendor: data.vendor.as_ptr(), 59 | url: data.url.as_ptr(), 60 | manual_url: data.manual_url.as_ptr(), 61 | support_url: data.support_url.as_ptr(), 62 | version: data.version.as_ptr(), 63 | description: data.description.as_ptr(), 64 | features: data.feature_pointers.as_ptr(), 65 | }; 66 | 67 | Self { 68 | data, 69 | raw, 70 | } 71 | } 72 | 73 | pub fn as_raw(&self) -> &clap_plugin_descriptor { 74 | &self.raw 75 | } 76 | 77 | pub fn id(&self) -> &CStr { 78 | &self.data.id 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/entry_point.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_char, c_void}; 2 | 3 | use clap_sys::{entry::clap_plugin_entry, version::CLAP_VERSION}; 4 | 5 | #[repr(transparent)] 6 | pub struct EntryPoint { 7 | _raw: clap_plugin_entry, 8 | } 9 | 10 | impl EntryPoint { 11 | pub const fn new( 12 | init: unsafe extern "C" fn(plugin_path: *const c_char) -> bool, 13 | deinit: unsafe extern "C" fn(), 14 | get_factory: unsafe extern "C" fn(factory_id: *const c_char) -> *const c_void, 15 | ) -> Self { 16 | Self { 17 | _raw: clap_plugin_entry { 18 | clap_version: CLAP_VERSION, 19 | init: Some(init), 20 | deinit: Some(deinit), 21 | get_factory: Some(get_factory), 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/event.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use clap_sys::events::{clap_event_note, clap_event_note_expression, clap_event_param_mod, clap_event_param_value, clap_input_events, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_MOD, CLAP_EVENT_PARAM_VALUE, CLAP_NOTE_EXPRESSION_TUNING}; 4 | 5 | use crate::{parameters::info::ParameterInfo, Event, ParameterId}; 6 | 7 | use super::parameters::map_parameter_value_from_clap; 8 | 9 | pub struct EventIterator<'a> { 10 | parameter_info: &'a BTreeMap, 11 | events: &'a clap_input_events, 12 | index: u32, 13 | } 14 | 15 | impl<'a> EventIterator<'a> { 16 | pub fn new(parameter_info: &'a BTreeMap, events: &'a clap_input_events) -> Self { 17 | Self { 18 | parameter_info, 19 | events, 20 | index: 0, 21 | } 22 | } 23 | } 24 | 25 | impl Iterator for EventIterator<'_> { 26 | type Item = Event; 27 | 28 | fn next(&mut self) -> Option { 29 | let events_size = unsafe { (self.events.size.unwrap())(self.events) }; 30 | 31 | loop { 32 | if self.index >= events_size { 33 | return None; 34 | } 35 | 36 | let header = unsafe { (self.events.get.unwrap())(self.events, self.index) }; 37 | self.index += 1; 38 | 39 | if unsafe { *header }.space_id != CLAP_CORE_EVENT_SPACE_ID { 40 | continue; 41 | } 42 | 43 | let event = match (unsafe { *header }).type_ { 44 | CLAP_EVENT_NOTE_ON => { 45 | let event = unsafe { &*(header as *const clap_event_note) }; 46 | 47 | Event::NoteOn { 48 | channel: event.channel, 49 | key: event.key, 50 | note: event.note_id, 51 | velocity: event.velocity, 52 | } 53 | } 54 | 55 | CLAP_EVENT_NOTE_OFF => { 56 | let event = unsafe { &*(header as *const clap_event_note) }; 57 | 58 | Event::NoteOff { 59 | channel: event.channel, 60 | key: event.key, 61 | note: event.note_id, 62 | velocity: event.velocity, 63 | } 64 | } 65 | 66 | CLAP_EVENT_NOTE_EXPRESSION => { 67 | let event = unsafe { &*(header as *const clap_event_note_expression) }; 68 | if event.expression_id != CLAP_NOTE_EXPRESSION_TUNING { 69 | continue; 70 | } 71 | 72 | Event::PitchBend { 73 | channel: event.channel, 74 | key: event.key, 75 | note: event.note_id, 76 | semitones: event.value, 77 | } 78 | } 79 | 80 | CLAP_EVENT_PARAM_VALUE => { 81 | let event = unsafe { &*(header as *const clap_event_param_value) }; 82 | let parameter_info = self.parameter_info.get(&event.param_id)?; 83 | 84 | let value = map_parameter_value_from_clap(parameter_info, event.value); 85 | 86 | Event::ParameterValue { 87 | sample_offset: event.header.time as _, 88 | id: event.param_id, 89 | value, 90 | } 91 | }, 92 | 93 | CLAP_EVENT_PARAM_MOD => { 94 | let event = unsafe { &*(header as *const clap_event_param_mod) }; 95 | let parameter_info = self.parameter_info.get(&event.param_id)?; 96 | 97 | let amount = map_parameter_value_from_clap(parameter_info, event.amount); 98 | 99 | Event::ParameterModulation { 100 | sample_offset: event.header.time as _, 101 | id: event.param_id, 102 | amount, 103 | } 104 | }, 105 | 106 | _ => { 107 | continue; 108 | } 109 | }; 110 | 111 | return Some(event); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions.rs: -------------------------------------------------------------------------------- 1 | pub mod audio_ports; 2 | pub mod gui; 3 | pub mod latency; 4 | pub mod note_ports; 5 | pub mod params; 6 | pub mod render; 7 | pub mod state; 8 | pub mod tail; 9 | pub mod timer_support; 10 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/audio_ports.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::audio_ports::{clap_audio_port_info, clap_plugin_audio_ports, CLAP_AUDIO_PORT_IS_MAIN, CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE, CLAP_PORT_STEREO}, id::CLAP_INVALID_ID, plugin::clap_plugin}; 4 | 5 | use crate::{clap::ClapPlugin, string::copy_str_to_char8}; 6 | 7 | #[repr(C)] 8 | pub struct AudioPorts { 9 | raw: clap_plugin_audio_ports, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl AudioPorts

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_audio_ports { 18 | count: Some(Self::count), 19 | get: Some(Self::get), 20 | }, 21 | 22 | _phantom_plugin: PhantomData, 23 | } 24 | } 25 | 26 | pub fn as_raw(&self) -> *const clap_plugin_audio_ports { 27 | &self.raw 28 | } 29 | 30 | // Number of ports, for either input or output 31 | // [main-thread] 32 | unsafe extern "C" fn count(_plugin: *const clap_plugin, is_input: bool) -> u32 { 33 | if is_input && P::HAS_AUX_INPUT { 34 | 2 35 | } else { 36 | 1 37 | } 38 | } 39 | 40 | // Get info about an audio port. 41 | // Returns true on success and stores the result into info. 42 | // [main-thread] 43 | unsafe extern "C" fn get( 44 | _plugin: *const clap_plugin, 45 | index: u32, 46 | is_input: bool, 47 | info: *mut clap_audio_port_info, 48 | ) -> bool 49 | { 50 | let info = unsafe { &mut *info }; 51 | 52 | info.id = index; 53 | info.channel_count = 2; 54 | info.port_type = CLAP_PORT_STEREO.as_ptr(); 55 | 56 | if index == 0 { 57 | info.flags = CLAP_AUDIO_PORT_IS_MAIN | CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE; 58 | info.in_place_pair = 0; 59 | 60 | copy_str_to_char8("Main", &mut info.name); 61 | } else { 62 | assert!(index == 1 && is_input && P::HAS_AUX_INPUT); 63 | info.flags = 0; 64 | info.in_place_pair = CLAP_INVALID_ID; 65 | 66 | copy_str_to_char8("Aux", &mut info.name); 67 | } 68 | 69 | true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/latency.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::latency::clap_plugin_latency, plugin::clap_plugin}; 4 | 5 | use crate::clap::{plugin_instance::PluginInstance, ClapPlugin}; 6 | 7 | #[repr(transparent)] 8 | pub struct Latency { 9 | raw: clap_plugin_latency, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl Latency

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_latency { 18 | get: Some(Self::get), 19 | }, 20 | 21 | _phantom_plugin: PhantomData, 22 | } 23 | } 24 | 25 | pub fn as_raw(&self) -> *const clap_plugin_latency { 26 | &self.raw 27 | } 28 | 29 | unsafe extern "C" fn get(plugin: *const clap_plugin) -> u32 { 30 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 31 | instance.plugin.as_ref().unwrap().latency() as _ 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/note_ports.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::note_ports::{clap_note_port_info, clap_plugin_note_ports, CLAP_NOTE_DIALECT_CLAP}, plugin::clap_plugin}; 4 | 5 | use crate::{clap::ClapPlugin, string::copy_str_to_char8}; 6 | 7 | #[repr(C)] 8 | pub struct NotePorts { 9 | raw: clap_plugin_note_ports, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl NotePorts

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_note_ports { 18 | count: Some(Self::count), 19 | get: Some(Self::get), 20 | }, 21 | 22 | _phantom_plugin: PhantomData, 23 | } 24 | } 25 | 26 | pub fn as_raw(&self) -> *const clap_plugin_note_ports { 27 | &self.raw 28 | } 29 | 30 | // Number of ports, for either input or output 31 | // [main-thread] 32 | unsafe extern "C" fn count(_plugin: *const clap_plugin, is_input: bool) -> u32 { 33 | if (is_input && P::HAS_NOTE_INPUT) || (!is_input && P::HAS_NOTE_OUTPUT) { 34 | 1 35 | } else { 36 | 0 37 | } 38 | } 39 | 40 | // Get info about an audio port. 41 | // Returns true on success and stores the result into info. 42 | // [main-thread] 43 | unsafe extern "C" fn get( 44 | _plugin: *const clap_plugin, 45 | index: u32, 46 | _is_input: bool, 47 | info: *mut clap_note_port_info, 48 | ) -> bool 49 | { 50 | let info = unsafe { &mut *info }; 51 | 52 | info.id = index; 53 | info.supported_dialects = CLAP_NOTE_DIALECT_CLAP; 54 | info.preferred_dialect = CLAP_NOTE_DIALECT_CLAP; 55 | copy_str_to_char8("Main", &mut info.name); 56 | 57 | true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/render.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::render::{clap_plugin_render, clap_plugin_render_mode, CLAP_RENDER_OFFLINE, CLAP_RENDER_REALTIME}, plugin::clap_plugin}; 4 | 5 | use crate::{clap::{plugin_instance::PluginInstance, ClapPlugin}, ProcessMode}; 6 | 7 | #[repr(transparent)] 8 | pub struct Render { 9 | raw: clap_plugin_render, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl Render

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_render { 18 | has_hard_realtime_requirement: Some(Self::has_hard_realtime_requirement), 19 | set: Some(Self::set), 20 | }, 21 | 22 | _phantom_plugin: PhantomData, 23 | } 24 | } 25 | 26 | pub fn as_raw(&self) -> *const clap_plugin_render { 27 | &self.raw 28 | } 29 | 30 | unsafe extern "C" fn has_hard_realtime_requirement(_plugin: *const clap_plugin) -> bool { 31 | false 32 | } 33 | 34 | unsafe extern "C" fn set(plugin: *const clap_plugin, mode: clap_plugin_render_mode) -> bool { 35 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 36 | instance.process_mode = match mode { 37 | CLAP_RENDER_REALTIME => ProcessMode::Realtime, 38 | CLAP_RENDER_OFFLINE => ProcessMode::Offline, 39 | _ => { return false; }, 40 | }; 41 | 42 | true 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/state.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::state::clap_plugin_state, plugin::clap_plugin, stream::{clap_istream, clap_ostream}}; 4 | 5 | use crate::clap::{plugin_instance::PluginInstance, stream::{InputStream, OutputStream}, ClapPlugin}; 6 | 7 | #[repr(transparent)] 8 | pub struct State { 9 | raw: clap_plugin_state, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl State

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_state { 18 | save: Some(Self::save), 19 | load: Some(Self::load), 20 | }, 21 | 22 | _phantom_plugin: PhantomData, 23 | } 24 | } 25 | 26 | pub fn as_raw(&self) -> *const clap_plugin_state { 27 | &self.raw 28 | } 29 | 30 | unsafe extern "C" fn save(plugin: *const clap_plugin, stream: *const clap_ostream) -> bool { 31 | let mut stream = OutputStream::new(stream); 32 | 33 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 34 | instance.process_events_to_plugin(); 35 | 36 | match instance.plugin.as_ref().unwrap().save_state(&mut stream) { 37 | Ok(_) => true, 38 | Err(e) => { 39 | println!("Error saving state: {:?}", e); 40 | false 41 | }, 42 | } 43 | }) 44 | } 45 | 46 | unsafe extern "C" fn load(plugin: *const clap_plugin, stream: *const clap_istream) -> bool { 47 | let mut stream = InputStream::new(stream); 48 | 49 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 50 | instance.process_events_to_plugin(); 51 | 52 | match instance.plugin.as_mut().unwrap().load_state(&mut stream) { 53 | Ok(_) => true, 54 | Err(e) => { 55 | println!("Error loading state: {e:?}"); 56 | false 57 | } 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/tail.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, sync::atomic::Ordering}; 2 | 3 | use clap_sys::{ext::tail::clap_plugin_tail, plugin::clap_plugin}; 4 | 5 | use crate::clap::{plugin_instance::PluginInstance, ClapPlugin}; 6 | 7 | #[repr(transparent)] 8 | pub struct Tail { 9 | raw: clap_plugin_tail, 10 | 11 | _phantom_plugin: PhantomData

, 12 | } 13 | 14 | impl Tail

{ 15 | pub const fn new() -> Self { 16 | Self { 17 | raw: clap_plugin_tail { 18 | get: Some(Self::get), 19 | }, 20 | 21 | _phantom_plugin: PhantomData, 22 | } 23 | } 24 | 25 | pub fn as_raw(&self) -> *const clap_plugin_tail { 26 | &self.raw 27 | } 28 | 29 | unsafe extern "C" fn get(plugin: *const clap_plugin) -> u32 { 30 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 31 | instance.audio_thread_state.tail.load(Ordering::Acquire) as _ 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/extensions/timer_support.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use clap_sys::{ext::timer_support::clap_plugin_timer_support, id::clap_id, plugin::clap_plugin}; 4 | 5 | use crate::clap::{plugin_instance::PluginInstance, ClapPlugin}; 6 | use crate::editor::Editor; 7 | 8 | #[repr(transparent)] 9 | pub struct TimerSupport { 10 | raw: clap_plugin_timer_support, 11 | 12 | _phantom_plugin: PhantomData

, 13 | } 14 | 15 | impl TimerSupport

{ 16 | pub const fn new() -> Self { 17 | Self { 18 | raw: clap_plugin_timer_support { 19 | on_timer: Some(Self::on_timer), 20 | }, 21 | 22 | _phantom_plugin: PhantomData, 23 | } 24 | } 25 | 26 | pub fn as_raw(&self) -> *const clap_plugin_timer_support { 27 | &self.raw 28 | } 29 | 30 | unsafe extern "C" fn on_timer(plugin: *const clap_plugin, _timer_id: clap_id) { 31 | PluginInstance::with_plugin_instance(plugin, |instance: &mut PluginInstance

| { 32 | if let Some(editor) = instance.editor.as_mut() { 33 | editor.on_frame(); 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/factory.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::{c_char, CStr}, marker::PhantomData, ptr::null}; 2 | 3 | use clap_sys::{factory::plugin_factory::clap_plugin_factory, host::clap_host, plugin::{clap_plugin, clap_plugin_descriptor}}; 4 | 5 | use super::{descriptor::Descriptor, plugin::ClapPlugin, plugin_instance::PluginInstance}; 6 | 7 | #[repr(C)] 8 | pub struct Factory { 9 | raw: clap_plugin_factory, 10 | count: usize, 11 | 12 | descriptor: Descriptor, 13 | 14 | _phantom_plugin: PhantomData

, 15 | } 16 | 17 | impl Factory

{ 18 | pub fn new() -> Self { 19 | Self { 20 | raw: clap_plugin_factory { 21 | get_plugin_count: Some(Self::get_plugin_count), 22 | get_plugin_descriptor: Some(Self::get_plugin_descriptor), 23 | create_plugin: Some(Self::create_plugin), 24 | }, 25 | count: 1, 26 | 27 | descriptor: Descriptor::new::

(), 28 | 29 | _phantom_plugin: PhantomData, 30 | } 31 | } 32 | 33 | pub fn as_raw(&self) -> *const clap_plugin_factory { 34 | &self.raw 35 | } 36 | 37 | pub fn count(&self) -> usize { 38 | self.count 39 | } 40 | 41 | pub fn add_ref(&mut self) -> usize { 42 | self.count += 1; 43 | self.count 44 | } 45 | 46 | pub fn remove_ref(&mut self) -> usize { 47 | assert!(self.count > 0); 48 | self.count -= 1; 49 | self.count 50 | } 51 | 52 | /// # Safety 53 | /// 54 | /// `factory_id` must be a valid pointer 55 | pub unsafe fn is_valid_factory_id(factory_id: *const c_char) -> bool { 56 | if factory_id.is_null() { 57 | return false; 58 | } 59 | 60 | if unsafe { CStr::from_ptr(factory_id) } != ::clap_sys::factory::plugin_factory::CLAP_PLUGIN_FACTORY_ID { 61 | return false; 62 | } 63 | 64 | true 65 | } 66 | 67 | unsafe extern "C" fn get_plugin_count(_factory: *const clap_plugin_factory) -> u32 { 68 | 1 69 | } 70 | 71 | unsafe extern "C" fn get_plugin_descriptor( 72 | factory: *const clap_plugin_factory, 73 | index: u32, 74 | ) -> *const clap_plugin_descriptor 75 | { 76 | let factory = unsafe { &*(factory as *const Self) }; 77 | 78 | if index == 0 { 79 | factory.descriptor.as_raw() 80 | } else { 81 | null() 82 | } 83 | } 84 | 85 | unsafe extern "C" fn create_plugin( 86 | factory: *const clap_plugin_factory, 87 | host: *const clap_host, 88 | plugin_id: *const c_char, 89 | ) -> *const clap_plugin 90 | { 91 | let factory = unsafe { &*(factory as *const Self) }; 92 | 93 | if plugin_id.is_null() { 94 | return null(); 95 | } 96 | if unsafe { CStr::from_ptr(plugin_id) } != factory.descriptor.id() { 97 | return null(); 98 | } 99 | 100 | let instance = Box::new(PluginInstance::

::new(&factory.descriptor, host)); 101 | Box::into_raw(instance) as _ 102 | } 103 | } 104 | 105 | impl Default for Factory

{ 106 | fn default() -> Self { 107 | Self::new() 108 | } 109 | } 110 | 111 | unsafe impl Send for Factory

{} 112 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/features.rs: -------------------------------------------------------------------------------- 1 | pub enum Feature { 2 | Analyzer, 3 | AudioEffect, 4 | Instrument, 5 | NoteDetector, 6 | NoteEffect, 7 | 8 | Chorus, 9 | Compressor, 10 | DeEsser, 11 | Delay, 12 | Distortion, 13 | Drum, 14 | DrumMachine, 15 | Equalizer, 16 | Expander, 17 | Filter, 18 | Flanger, 19 | FrequencyShifter, 20 | Gate, 21 | Glitch, 22 | Granular, 23 | Limiter, 24 | Mastering, 25 | Mixing, 26 | MultiEffects, 27 | Phaser, 28 | PhaseVocoder, 29 | PitchCorrection, 30 | PitchShifter, 31 | Restoration, 32 | Reverb, 33 | Sampler, 34 | Synthesizer, 35 | TransientShaper, 36 | Tremolo, 37 | Utility, 38 | 39 | Ambisonic, 40 | Mono, 41 | Stereo, 42 | Surround, 43 | } 44 | 45 | impl Feature { 46 | pub fn to_str(&self) -> &str { 47 | match self { 48 | Feature::Analyzer => "analyzer", 49 | Feature::AudioEffect => "audio-effect", 50 | Feature::Instrument => "instrument", 51 | Feature::NoteDetector => "note-detector", 52 | Feature::NoteEffect => "note-effect", 53 | 54 | Feature::Chorus => "chorus", 55 | Feature::Compressor => "compressor", 56 | Feature::DeEsser => "de-esser", 57 | Feature::Delay => "delay", 58 | Feature::Distortion => "distortion", 59 | Feature::Drum => "drum", 60 | Feature::DrumMachine => "drum-machine", 61 | Feature::Equalizer => "equalizer", 62 | Feature::Expander => "expander", 63 | Feature::Filter => "filter", 64 | Feature::Flanger => "flanger", 65 | Feature::FrequencyShifter => "frequency-shifter", 66 | Feature::Gate => "gate", 67 | Feature::Glitch => "glitch", 68 | Feature::Granular => "granular", 69 | Feature::Limiter => "limiter", 70 | Feature::Mastering => "mastering", 71 | Feature::Mixing => "mixing", 72 | Feature::MultiEffects => "multi-effects", 73 | Feature::Phaser => "phaser", 74 | Feature::PhaseVocoder => "phase-vocoder", 75 | Feature::PitchCorrection => "pitch-correction", 76 | Feature::PitchShifter => "pitch-shifter", 77 | Feature::Restoration => "restoration", 78 | Feature::Reverb => "reverb", 79 | Feature::Sampler => "sampler", 80 | Feature::Synthesizer => "synthesizer", 81 | Feature::TransientShaper => "transient-shaper", 82 | Feature::Tremolo => "tremolo", 83 | Feature::Utility => "utility", 84 | 85 | Feature::Ambisonic => "ambisonic", 86 | Feature::Mono => "mono", 87 | Feature::Stereo => "stereo", 88 | Feature::Surround => "surround", 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/host.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::{atomic::Ordering, Arc}}; 2 | 3 | use clap_sys::{ext::{gui::clap_host_gui, params::clap_host_params, state::clap_host_state}, host::clap_host}; 4 | 5 | use crate::{Host, ParameterId, ParameterValue}; 6 | 7 | use super::parameters::ParameterEventMap; 8 | 9 | pub struct ClapHost { 10 | raw: *const clap_host, 11 | host_ext_gui: *const clap_host_gui, 12 | host_ext_params: *const clap_host_params, 13 | host_ext_state: *const clap_host_state, 14 | parameter_event_map: Arc, 15 | } 16 | 17 | impl ClapHost { 18 | pub fn new( 19 | raw: *const clap_host, 20 | host_ext_gui: *const clap_host_gui, 21 | host_ext_params: *const clap_host_params, 22 | host_ext_state: *const clap_host_state, 23 | parameter_event_map: Arc, 24 | ) -> Self { 25 | assert!(!raw.is_null()); 26 | 27 | Self { 28 | raw, 29 | host_ext_gui, 30 | host_ext_params, 31 | host_ext_state, 32 | parameter_event_map, 33 | } 34 | } 35 | } 36 | 37 | impl Host for ClapHost { 38 | fn can_resize(&self) -> bool { 39 | true 40 | } 41 | 42 | fn resize_view(&self, width: f64, height: f64) -> bool { 43 | if self.host_ext_gui.is_null() { 44 | return false; 45 | } 46 | 47 | unsafe { ((*self.host_ext_gui).request_resize.unwrap())(self.raw, width as _, height as _) } 48 | } 49 | 50 | fn start_parameter_change(&self, id: ParameterId) { 51 | self.parameter_event_map.parameter_event_info(id).change_started.store(true, Ordering::Release); 52 | 53 | if !self.host_ext_params.is_null() { 54 | unsafe { ((*self.host_ext_params).request_flush.unwrap())(self.raw) }; 55 | } 56 | } 57 | 58 | fn change_parameter_value(&self, id: ParameterId, normalized: ParameterValue) { 59 | let parameter_event_info = self.parameter_event_map.parameter_event_info(id); 60 | 61 | parameter_event_info.value.store(normalized, Ordering::Release); 62 | parameter_event_info.changed.store(true, Ordering::Release); 63 | 64 | if !self.host_ext_params.is_null() { 65 | unsafe { ((*self.host_ext_params).request_flush.unwrap())(self.raw) }; 66 | } 67 | } 68 | 69 | fn end_parameter_change(&self, id: ParameterId) { 70 | self.parameter_event_map.parameter_event_info(id).change_ended.store(true, Ordering::Release); 71 | 72 | if !self.host_ext_params.is_null() { 73 | unsafe { ((*self.host_ext_params).request_flush.unwrap())(self.raw) }; 74 | } 75 | } 76 | 77 | fn mark_state_dirty(&self) { 78 | if !self.host_ext_state.is_null() { 79 | unsafe { ((*self.host_ext_state).mark_dirty.unwrap())(self.raw) }; 80 | } 81 | } 82 | } 83 | 84 | /// SAFETY: clap_host functions are thread-safe 85 | unsafe impl Send for ClapHost {} 86 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! export_clap { 3 | ($plugin:ty) => { 4 | static FACTORY: ::std::sync::Mutex>> = ::std::sync::Mutex::new(None); 5 | 6 | unsafe extern "C" fn init(_plugin_path: *const ::std::ffi::c_char) -> bool { 7 | let mut factory = FACTORY.lock().unwrap(); 8 | 9 | match factory.as_mut() { 10 | Some(factory) => { 11 | factory.add_ref(); 12 | }, 13 | 14 | None => { 15 | *factory = Some(::plinth_plugin::clap::Factory::<$plugin>::new()); 16 | } 17 | } 18 | 19 | true 20 | } 21 | 22 | unsafe extern "C" fn deinit() { 23 | let mut maybe_factory = FACTORY.lock().unwrap(); 24 | 25 | match maybe_factory.as_mut() { 26 | Some(factory) => { 27 | if factory.remove_ref() == 0 { 28 | *maybe_factory = None; 29 | } 30 | }, 31 | 32 | None => { 33 | ::plinth_plugin::log::warn!("deinit() called more than once"); 34 | panic!(); 35 | }, 36 | } 37 | } 38 | 39 | unsafe extern "C" fn get_factory(factory_id: *const ::std::ffi::c_char) -> *const ::std::ffi::c_void { 40 | if unsafe { !::plinth_plugin::clap::Factory::<$plugin>::is_valid_factory_id(factory_id) } { 41 | return ::std::ptr::null(); 42 | } 43 | 44 | let factory = FACTORY.lock().unwrap(); 45 | let Some(factory) = factory.as_ref() else { 46 | return ::std::ptr::null(); 47 | }; 48 | 49 | factory.as_raw() as _ 50 | } 51 | 52 | #[unsafe(no_mangle)] 53 | #[allow(non_snake_case)] 54 | #[allow(non_upper_case_globals)] 55 | static clap_entry: ::plinth_plugin::clap::EntryPoint = ::plinth_plugin::clap::EntryPoint::new(init, deinit, get_factory); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::plugin::Plugin; 2 | 3 | use super::features::Feature; 4 | 5 | pub trait ClapPlugin : Plugin { 6 | const CLAP_ID: &'static str; 7 | 8 | const FEATURES: &'static [Feature]; 9 | 10 | const MANUAL_URL: Option<&'static str> = None; 11 | const SUPPORT_URL: Option<&'static str> = None; 12 | const DESCRIPTION: Option<&'static str> = None; 13 | 14 | const EVENT_QUEUE_LEN: usize = 1024; 15 | } 16 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | use clap_sys::stream::{clap_istream, clap_ostream}; 4 | 5 | pub struct InputStream { 6 | raw: *const clap_istream, 7 | } 8 | 9 | impl InputStream { 10 | pub fn new(raw: *const clap_istream) -> Self { 11 | Self { 12 | raw, 13 | } 14 | } 15 | } 16 | 17 | impl Read for InputStream { 18 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 19 | let mut index = 0; 20 | 21 | while index < buf.len() { 22 | let remaining_bytes = buf.len() - index; 23 | 24 | let bytes_read = unsafe { 25 | let istream = &*self.raw; 26 | (istream.read.unwrap())(self.raw, buf.as_ptr().add(index) as _, remaining_bytes as _) 27 | }; 28 | 29 | if bytes_read < 0 { 30 | return Err(std::io::Error::other("CLAP stream read error")); 31 | } 32 | if bytes_read == 0 { 33 | break; 34 | } 35 | 36 | index += bytes_read as usize; 37 | } 38 | 39 | 40 | Ok(index) 41 | } 42 | } 43 | 44 | pub struct OutputStream { 45 | raw: *const clap_ostream, 46 | } 47 | 48 | impl OutputStream { 49 | pub fn new(raw: *const clap_ostream) -> Self { 50 | Self { 51 | raw, 52 | } 53 | } 54 | } 55 | 56 | impl Write for OutputStream { 57 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 58 | let mut index = 0; 59 | 60 | while index < buf.len() { 61 | let remaining_bytes = buf.len() - index; 62 | 63 | let bytes_written = unsafe { 64 | let ostream = &*self.raw; 65 | (ostream.write.unwrap())(self.raw, buf.as_ptr().add(index) as _, remaining_bytes as _) 66 | }; 67 | 68 | if bytes_written < 0 { 69 | return Err(std::io::Error::other("CLAP stream write error")); 70 | } 71 | if bytes_written == 0 { 72 | break; 73 | } 74 | 75 | index += bytes_written as usize; 76 | } 77 | 78 | Ok(index) 79 | } 80 | 81 | fn flush(&mut self) -> std::io::Result<()> { 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/clap/transport.rs: -------------------------------------------------------------------------------- 1 | use clap_sys::{events::{clap_event_transport, CLAP_TRANSPORT_IS_PLAYING}, fixedpoint::CLAP_SECTIME_FACTOR}; 2 | 3 | use crate::Transport; 4 | 5 | pub fn convert_transport(transport: &clap_event_transport, sample_rate: f64) -> Transport { 6 | let position_seconds = transport.song_pos_seconds as f64 / CLAP_SECTIME_FACTOR as f64; 7 | 8 | Transport { 9 | playing: transport.flags & CLAP_TRANSPORT_IS_PLAYING > 0, 10 | tempo: transport.tempo, 11 | position_samples: f64::round(position_seconds * sample_rate) as _, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3.rs: -------------------------------------------------------------------------------- 1 | mod component; 2 | mod error; 3 | mod event; 4 | mod factory; 5 | mod host; 6 | mod macros; 7 | mod parameters; 8 | mod plugin; 9 | mod stream; 10 | mod subcategories; 11 | mod transport; 12 | mod view; 13 | 14 | pub use error::Error; 15 | pub use factory::Factory; 16 | pub use plugin::Vst3Plugin; 17 | pub use subcategories::Subcategory; 18 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/error.rs: -------------------------------------------------------------------------------- 1 | use num_derive::FromPrimitive; 2 | 3 | #[repr(i32)] 4 | #[derive(Debug, FromPrimitive, thiserror::Error)] 5 | pub enum Error { 6 | #[error("No interface")] 7 | NoInterface = -1, 8 | #[error("False")] 9 | ResultFalse = 1, 10 | #[error("Invalid argument")] 11 | InvalidArgument = 2, 12 | #[error("Not implemented")] 13 | NotImplemented = 3, 14 | #[error("Internal error")] 15 | InternalError = 4, 16 | #[error("Not initialized")] 17 | NotInitialized = 5, 18 | #[error("Out of memory")] 19 | OutOfMemory = 6, 20 | } 21 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/event.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use vst3::{ComRef, Steinberg::{kResultOk, Vst::{self, IEventList, IEventListTrait}}}; 4 | 5 | use crate::Event; 6 | 7 | pub struct EventIterator<'a> { 8 | event_list: Option>, 9 | index: usize, 10 | } 11 | 12 | impl EventIterator<'_> { 13 | pub fn new(event_list: *mut IEventList) -> Self { 14 | Self { 15 | event_list: unsafe { ComRef::from_raw(event_list) }, 16 | index: 0, 17 | } 18 | } 19 | } 20 | 21 | impl Iterator for EventIterator<'_> { 22 | type Item = Event; 23 | 24 | fn next(&mut self) -> Option { 25 | let event_list = self.event_list?; 26 | 27 | if self.index >= unsafe { event_list.getEventCount() } as usize { 28 | return None; 29 | } 30 | 31 | let mut event: vst3::Steinberg::Vst::Event = unsafe { mem::zeroed() }; 32 | let result = unsafe { event_list.getEvent(self.index as _, &mut event) }; 33 | if result != kResultOk { 34 | return None; 35 | } 36 | 37 | self.index += 1; 38 | 39 | match event.r#type as _ { 40 | Vst::Event_::EventTypes_::kNoteOnEvent => unsafe { 41 | Some(Event::NoteOn { 42 | channel: event.__field0.noteOn.channel, 43 | key: event.__field0.noteOn.pitch, 44 | note: event.__field0.noteOn.noteId, 45 | velocity: event.__field0.noteOn.velocity as _, 46 | }) 47 | }, 48 | 49 | Vst::Event_::EventTypes_::kNoteOffEvent => unsafe { 50 | Some(Event::NoteOff { 51 | channel: event.__field0.noteOff.channel, 52 | key: event.__field0.noteOff.pitch, 53 | note: event.__field0.noteOn.noteId, 54 | velocity: event.__field0.noteOff.velocity as _, 55 | }) 56 | }, 57 | 58 | _ => None 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/factory.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::{c_void, CStr}, marker::PhantomData}; 2 | 3 | use vst3::{ComWrapper, Steinberg::{int32, kInvalidArgument, kResultOk, tresult, FIDString, FUnknown, IPluginFactory, IPluginFactory2, IPluginFactory2Trait, IPluginFactory3, IPluginFactory3Trait, IPluginFactoryTrait, PClassInfo, PClassInfo2, PClassInfoW, PClassInfo_::ClassCardinality_::kManyInstances, PFactoryInfo, PFactoryInfo_, Vst::SDKVersionString, TUID}}; 4 | 5 | use crate::string::{copy_str_to_char16, copy_str_to_char8, copy_u128_to_char8}; 6 | 7 | use super::{plugin::Vst3Plugin, component::PluginComponent}; 8 | 9 | pub struct Factory { 10 | _phantom_plugin: PhantomData

, 11 | } 12 | 13 | impl Factory

{ 14 | // This is a bit special 15 | #[allow(clippy::new_ret_no_self)] 16 | pub fn new() -> *mut IPluginFactory { 17 | let factory = Self { 18 | _phantom_plugin: PhantomData, 19 | }; 20 | 21 | ComWrapper::new(factory) 22 | .to_com_ptr::() 23 | .unwrap() 24 | .into_raw() as _ 25 | } 26 | } 27 | 28 | impl vst3::Class for Factory

{ 29 | type Interfaces = (IPluginFactory, IPluginFactory2, IPluginFactory3); 30 | } 31 | 32 | #[allow(non_snake_case)] 33 | impl IPluginFactoryTrait for Factory

{ 34 | unsafe fn getFactoryInfo(&self, info: *mut PFactoryInfo) -> tresult { 35 | let mut local_info: PFactoryInfo = unsafe { std::mem::zeroed() }; 36 | 37 | local_info.flags = PFactoryInfo_::FactoryFlags_::kUnicode as _; 38 | 39 | copy_str_to_char8(P::VENDOR, &mut local_info.vendor); 40 | 41 | if let Some(url) = P::URL { 42 | copy_str_to_char8(url, &mut local_info.url); 43 | } else { 44 | local_info.url.fill(0); 45 | } 46 | 47 | if let Some(email) = P::EMAIL { 48 | copy_str_to_char8(email, &mut local_info.email); 49 | } else { 50 | local_info.email.fill(0); 51 | } 52 | 53 | // We have to do a workaround like this for FL Studio which is giving us unaligned addresses 54 | unsafe { std::ptr::write_unaligned(info, local_info) }; 55 | 56 | kResultOk 57 | } 58 | 59 | unsafe fn countClasses(&self) -> int32 { 60 | 1 61 | } 62 | 63 | unsafe fn getClassInfo(&self, index: int32, info: *mut PClassInfo) -> tresult { 64 | if index != 0 { 65 | return kInvalidArgument; 66 | } 67 | 68 | let mut local_info: PClassInfo = unsafe { std::mem::zeroed() }; 69 | local_info.cardinality = kManyInstances as _; 70 | 71 | copy_u128_to_char8(&P::CLASS_ID, &mut local_info.cid); 72 | copy_str_to_char8(P::NAME, &mut local_info.name); 73 | 74 | copy_str_to_char8("Audio Module Class", &mut local_info.category); 75 | 76 | // We have to do a workaround like this for FL Studio which is giving us unaligned addresses 77 | unsafe { std::ptr::write_unaligned(info, local_info) }; 78 | 79 | kResultOk 80 | } 81 | 82 | unsafe fn createInstance(&self, cid: FIDString, iid: FIDString, obj: *mut *mut c_void) -> tresult { 83 | if cid.is_null() { 84 | return kInvalidArgument; 85 | } 86 | 87 | let bytes = unsafe { std::slice::from_raw_parts(cid, 16) }; 88 | let bytes_array: [u8; 16] = std::array::from_fn(|i| bytes[i] as u8); 89 | let cid = u128::from_be_bytes(bytes_array); 90 | 91 | if cid == P::CLASS_ID { 92 | let instance = ComWrapper::new(PluginComponent::

::new()); 93 | let unknown = instance.as_com_ref::().unwrap(); 94 | let ptr = unknown.as_ptr(); 95 | 96 | unsafe { ((*(*ptr).vtbl).queryInterface)(ptr, iid as *const TUID, obj) } 97 | } else { 98 | kInvalidArgument 99 | } 100 | } 101 | } 102 | 103 | #[allow(non_snake_case)] 104 | impl IPluginFactory2Trait for Factory

{ 105 | unsafe fn getClassInfo2(&self, index: int32, info: *mut PClassInfo2) -> tresult { 106 | if index != 0 { 107 | return kInvalidArgument; 108 | } 109 | 110 | let mut local_info: PClassInfo2 = unsafe { std::mem::zeroed() }; 111 | local_info.cardinality = kManyInstances as _; 112 | 113 | copy_u128_to_char8(&P::CLASS_ID, &mut local_info.cid); 114 | copy_str_to_char8(P::NAME, &mut local_info.name); 115 | copy_str_to_char8(P::VERSION, &mut local_info.version); 116 | 117 | copy_str_to_char8("Audio Module Class", &mut local_info.category); 118 | copy_str_to_char8(unsafe { CStr::from_ptr(SDKVersionString).to_str().unwrap() }, &mut local_info.sdkVersion); 119 | 120 | // We have to do a workaround like this for FL Studio which is giving us unaligned addresses 121 | unsafe { std::ptr::write_unaligned(info, local_info) }; 122 | 123 | kResultOk 124 | } 125 | } 126 | 127 | #[allow(non_snake_case)] 128 | impl IPluginFactory3Trait for Factory

{ 129 | unsafe fn getClassInfoUnicode(&self, index: int32, info: *mut PClassInfoW) -> tresult { 130 | if index != 0 { 131 | return kInvalidArgument; 132 | } 133 | 134 | let mut local_info: PClassInfoW = unsafe { std::mem::zeroed() }; 135 | local_info.cardinality = kManyInstances as _; 136 | 137 | copy_u128_to_char8(&P::CLASS_ID, &mut local_info.cid); 138 | copy_str_to_char16(P::NAME, &mut local_info.name); 139 | copy_str_to_char16(P::VERSION, &mut local_info.version); 140 | 141 | copy_str_to_char8("Audio Module Class", &mut local_info.category); 142 | copy_str_to_char16(unsafe { CStr::from_ptr(SDKVersionString).to_str().unwrap() }, &mut local_info.sdkVersion); 143 | 144 | let subcategory_string = P::SUBCATEGORIES 145 | .iter() 146 | .map(|subcategory| subcategory.to_str()) 147 | .collect::>() 148 | .join("|"); 149 | copy_str_to_char8(&subcategory_string, &mut local_info.subCategories); 150 | 151 | // We have to do a workaround like this for FL Studio which is giving us unaligned addresses 152 | unsafe { std::ptr::write_unaligned(info, local_info) }; 153 | 154 | kResultOk 155 | } 156 | 157 | unsafe fn setHostContext(&self, _context: *mut FUnknown) -> tresult { 158 | kResultOk 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/host.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use vst3::{ComPtr, Steinberg::{kResultOk, IPlugFrameTrait, IPlugView, ViewRect, Vst::{IComponentHandler, IComponentHandler2, IComponentHandler2Trait, IComponentHandlerTrait}}}; 4 | 5 | use crate::{host::Host, parameters::ParameterValue, ParameterId, Parameters, Plugin}; 6 | 7 | use super::view::ViewContext; 8 | 9 | pub struct Vst3Host { 10 | plugin: Rc>>, 11 | component_handler: Rc>>>, 12 | plug_view: ComPtr, 13 | view_context: Rc>, 14 | } 15 | 16 | impl Vst3Host

{ 17 | pub fn new( 18 | plugin: Rc>>, 19 | handler: Rc>>>, 20 | plug_view: ComPtr, 21 | view_context: Rc>, 22 | ) -> Self { 23 | Self { 24 | plugin, 25 | component_handler: handler, 26 | plug_view, 27 | view_context, 28 | } 29 | } 30 | } 31 | 32 | impl Host for Vst3Host

{ 33 | fn can_resize(&self) -> bool { 34 | true 35 | } 36 | 37 | fn resize_view(&self, width: f64, height: f64) -> bool { 38 | let view_context = self.view_context.borrow(); 39 | let Some(frame) = view_context.frame.as_ref() else { 40 | return false; 41 | }; 42 | 43 | let mut size = ViewRect { 44 | left: 0, 45 | top: 0, 46 | right: width as _, 47 | bottom: height as _, 48 | }; 49 | 50 | let result = unsafe { frame.resizeView(self.plug_view.as_ptr(), &mut size) }; 51 | result == kResultOk 52 | } 53 | 54 | fn start_parameter_change(&self, id: ParameterId) { 55 | if let Some(handler) = self.component_handler.borrow_mut().as_mut() { 56 | unsafe { handler.beginEdit(id) }; 57 | } 58 | } 59 | 60 | fn change_parameter_value(&self, id: ParameterId, normalized: ParameterValue) { 61 | let plugin = self.plugin.borrow(); 62 | let Some(plugin) = plugin.as_ref() else { 63 | return; 64 | }; 65 | 66 | plugin.with_parameters(|parameters| { 67 | let parameter = parameters.get(id).unwrap(); 68 | parameter.set_normalized_value(normalized).unwrap(); 69 | }); 70 | 71 | if let Some(handler) = self.component_handler.borrow_mut().as_mut() { 72 | unsafe { handler.performEdit(id, normalized) }; 73 | } 74 | } 75 | 76 | fn end_parameter_change(&self, id: ParameterId) { 77 | if let Some(handler) = self.component_handler.borrow_mut().as_mut() { 78 | unsafe { handler.endEdit(id) }; 79 | } 80 | } 81 | 82 | fn mark_state_dirty(&self) { 83 | if let Some(handler) = self.component_handler.borrow_mut().as_mut() { 84 | if let Some(handler2) = handler.cast::() { 85 | unsafe { handler2.setDirty(1) }; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! export_vst3 { 3 | ($plugin:ty) => { 4 | #[unsafe(no_mangle)] 5 | pub extern "system" fn GetPluginFactory() -> *mut ::std::ffi::c_void { 6 | ::plinth_plugin::vst3::Factory::<$plugin>::new() as _ 7 | } 8 | 9 | #[cfg(target_os="windows")] 10 | #[unsafe(no_mangle)] 11 | pub extern "system" fn InitDll() -> bool { 12 | true 13 | } 14 | 15 | #[cfg(target_os="windows")] 16 | #[unsafe(no_mangle)] 17 | pub extern "system" fn ExitDll() -> bool { 18 | true 19 | } 20 | 21 | #[cfg(target_os="macos")] 22 | #[unsafe(no_mangle)] 23 | pub extern "system" fn bundleEntry(_bundle: *mut std::ffi::c_void) -> bool { 24 | true 25 | } 26 | 27 | #[cfg(target_os="macos")] 28 | #[unsafe(no_mangle)] 29 | pub extern "system" fn bundleExit() -> bool { 30 | true 31 | } 32 | 33 | #[cfg(target_os="linux")] 34 | #[unsafe(no_mangle)] 35 | pub extern "system" fn ModuleEntry(shared_library_handle: *mut std::ffi::c_void) -> bool { 36 | true 37 | } 38 | 39 | #[cfg(target_os="linux")] 40 | #[unsafe(no_mangle)] 41 | pub extern "system" fn ModuleExit() -> bool { 42 | true 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/parameters.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use vst3::{ComRef, Steinberg::{kResultOk, Vst::{IParamValueQueueTrait, IParameterChanges, IParameterChangesTrait}}}; 4 | 5 | use crate::event::Event; 6 | 7 | pub struct ParameterChangeIterator<'a> { 8 | parameter_changes: Option>, 9 | offset: usize, 10 | index: usize, 11 | finished: bool, 12 | } 13 | 14 | impl ParameterChangeIterator<'_> { 15 | pub fn new(parameter_changes: *mut IParameterChanges) -> Self { 16 | Self { 17 | parameter_changes: unsafe { ComRef::from_raw(parameter_changes) }, 18 | offset: 0, 19 | index: 0, 20 | finished: false, 21 | } 22 | } 23 | } 24 | 25 | impl Iterator for ParameterChangeIterator<'_> { 26 | type Item = Event; 27 | 28 | fn next(&mut self) -> Option { 29 | if self.finished { 30 | return None; 31 | } 32 | let parameter_changes = self.parameter_changes?; 33 | 34 | let parameter_count = unsafe { parameter_changes.getParameterCount() }; 35 | assert!(parameter_count >= 0); 36 | if parameter_count == 0 { 37 | return None; 38 | } 39 | 40 | let current_offset = self.offset; 41 | let current_index = self.index; 42 | let mut nth = 0; 43 | 44 | let event = (0..unsafe { parameter_changes.getParameterCount() }) 45 | .flat_map(|parameter_index| { 46 | let Some(value_queue) = (unsafe { ComRef::from_raw(parameter_changes.getParameterData(parameter_index)) }) else { 47 | panic!(); 48 | }; 49 | 50 | let id = unsafe { value_queue.getParameterId() }; 51 | 52 | (0..unsafe { value_queue.getPointCount() }) 53 | .filter_map(move |point_index| { 54 | let mut offset = 0; 55 | let mut value = 0.0; 56 | let result = unsafe { value_queue.getPoint(point_index, &mut offset, &mut value) }; 57 | if result != kResultOk { 58 | panic!(); 59 | } 60 | 61 | assert!(offset >= 0); 62 | let offset = offset as usize; 63 | 64 | match offset.cmp(¤t_offset) { 65 | cmp::Ordering::Equal => { 66 | if nth >= current_index { 67 | Some((id, offset, value)) 68 | } else { 69 | nth += 1; 70 | None 71 | } 72 | }, 73 | 74 | cmp::Ordering::Greater => Some((id, offset, value)), 75 | 76 | cmp::Ordering::Less => None, 77 | } 78 | }) 79 | }) 80 | .filter(|(_, offset, _)| *offset >= current_offset) 81 | .min_by_key(|(_, offset, _)| *offset); 82 | 83 | let Some(event) = event else { 84 | self.finished = true; 85 | return None; 86 | }; 87 | 88 | let (id, offset, value) = event; 89 | 90 | if offset > self.offset { 91 | self.offset = offset; 92 | self.index = 0; 93 | } else { 94 | self.index += 1; 95 | } 96 | 97 | let event = Event::ParameterValue { 98 | sample_offset: offset, 99 | id, 100 | value, 101 | }; 102 | 103 | Some(event) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::plugin::Plugin; 2 | 3 | use super::subcategories::Subcategory; 4 | 5 | pub trait Vst3Plugin : Plugin { 6 | const CLASS_ID: u128; 7 | const SUBCATEGORIES: &'static [Subcategory]; 8 | 9 | const EMAIL: Option<&'static str> = None; 10 | } 11 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | use num_traits::FromPrimitive; 4 | use vst3::{ComRef, Steinberg::{kResultOk, IBStream, IBStreamTrait}}; 5 | 6 | use super::Error; 7 | 8 | pub struct Stream<'a> { 9 | raw: ComRef<'a, IBStream>, 10 | } 11 | 12 | impl Stream<'_> { 13 | pub fn new(raw: *mut IBStream) -> Option { 14 | let raw = (unsafe { ComRef::from_raw(raw) })?; 15 | 16 | Some(Self { 17 | raw, 18 | }) 19 | } 20 | } 21 | 22 | impl Read for Stream<'_> { 23 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 24 | let mut total_bytes_read = 0; 25 | while total_bytes_read < buf.len() { 26 | let mut bytes_read = 0; 27 | let result = unsafe { self.raw.read(buf.as_mut_ptr() as _, buf.len() as _, &mut bytes_read) }; 28 | 29 | if bytes_read <= 0 { 30 | break; 31 | } 32 | 33 | if result == kResultOk { 34 | total_bytes_read += bytes_read as usize; 35 | } else { 36 | return Err(std::io::Error::other(Error::from_i32(result).unwrap())); 37 | } 38 | } 39 | 40 | Ok(total_bytes_read) 41 | } 42 | } 43 | 44 | impl Write for Stream<'_> { 45 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 46 | let mut total_bytes_written = 0; 47 | 48 | while total_bytes_written < buf.len() { 49 | let mut bytes_written = 0; 50 | let result = unsafe { self.raw.write(buf.as_ptr() as _, buf.len() as _, &mut bytes_written) }; 51 | 52 | if bytes_written <= 0 { 53 | break; 54 | } 55 | 56 | if result == kResultOk { 57 | total_bytes_written += bytes_written as usize; 58 | } else { 59 | return Err(std::io::Error::other(Error::from_i32(result).unwrap())); 60 | } 61 | } 62 | 63 | Ok(total_bytes_written) 64 | } 65 | 66 | fn flush(&mut self) -> std::io::Result<()> { 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/subcategories.rs: -------------------------------------------------------------------------------- 1 | pub enum Subcategory { 2 | Fx, 3 | Instrument, 4 | 5 | Analyzer, 6 | Delay, 7 | Distortion, 8 | Drum, 9 | Dynamics, 10 | Eq, 11 | External, 12 | Filter, 13 | Generator, 14 | Mastering, 15 | Modulation, 16 | Network, 17 | Piano, 18 | PitchShift, 19 | Restoration, 20 | Reverb, 21 | Sampler, 22 | Spatial, 23 | Synth, 24 | Tools, 25 | UpDownMix, 26 | 27 | Ambisonics, 28 | Mono, 29 | Stereo, 30 | Surround, 31 | } 32 | 33 | impl Subcategory { 34 | pub fn to_str(&self) -> &str { 35 | match self { 36 | Subcategory::Fx => "Fx", 37 | Subcategory::Instrument => "Instrument", 38 | 39 | Subcategory::Analyzer => "Analyzer", 40 | Subcategory::Delay => "Delay", 41 | Subcategory::Distortion => "Distortion", 42 | Subcategory::Drum => "Drum", 43 | Subcategory::Dynamics => "Dynamics", 44 | Subcategory::Eq => "EQ", 45 | Subcategory::External => "External", 46 | Subcategory::Filter => "Filter", 47 | Subcategory::Generator => "Generator", 48 | Subcategory::Mastering => "Mastering", 49 | Subcategory::Modulation => "Modulation", 50 | Subcategory::Network => "Network", 51 | Subcategory::Piano => "Piano", 52 | Subcategory::PitchShift => "Pitch Shift", 53 | Subcategory::Restoration => "Restoration", 54 | Subcategory::Reverb => "Reverb", 55 | Subcategory::Sampler => "Sampler", 56 | Subcategory::Spatial => "Spatial", 57 | Subcategory::Synth => "Synth", 58 | Subcategory::Tools => "Tools", 59 | Subcategory::UpDownMix => "Up-Downmix", 60 | 61 | Subcategory::Ambisonics => "Ambisonics", 62 | Subcategory::Mono => "Mono", 63 | Subcategory::Stereo => "Stereo", 64 | Subcategory::Surround => "Surround", 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plinth-plugin/src/formats/vst3/transport.rs: -------------------------------------------------------------------------------- 1 | use vst3::Steinberg::Vst::{ProcessContext, ProcessContext_::StatesAndFlags_::kPlaying}; 2 | 3 | use crate::Transport; 4 | 5 | impl From<&ProcessContext> for Transport { 6 | fn from(context: &ProcessContext) -> Self { 7 | Self { 8 | // This cast is needed on some platforms 9 | #[allow(clippy::unnecessary_cast)] 10 | playing: context.state & kPlaying as u32 > 0, 11 | tempo: context.tempo, 12 | position_samples: context.projectTimeSamples, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plinth-plugin/src/host.rs: -------------------------------------------------------------------------------- 1 | use crate::{parameters::ParameterValue, ParameterId}; 2 | 3 | #[derive(Clone)] 4 | pub struct HostInfo { 5 | pub name: Option, 6 | } 7 | 8 | pub trait Host { 9 | fn can_resize(&self) -> bool; 10 | 11 | /// Return true if the resize was accepted 12 | fn resize_view(&self, width: f64, height: f64) -> bool; 13 | 14 | fn start_parameter_change(&self, id: ParameterId); 15 | fn change_parameter_value(&self, id: ParameterId, normalized: ParameterValue); 16 | fn end_parameter_change(&self, id: ParameterId); 17 | 18 | fn mark_state_dirty(&self); 19 | } 20 | -------------------------------------------------------------------------------- /plinth-plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use editor::{Editor, NoEditor}; 2 | pub use event::Event; 3 | pub use host::{Host, HostInfo}; 4 | pub use formats::{clap, vst3}; 5 | pub use parameters::{Parameters, ParameterId, ParameterValue}; 6 | pub use parameters::bool::{BoolParameter, BoolFormatter}; 7 | pub use parameters::enums::{Enum, EnumParameter}; 8 | pub use parameters::float::{FloatParameter, LinearFloatRange, LogFloatRange, PowFloatRange, FloatFormatter, HzFormatter}; 9 | pub use parameters::formatter::ParameterFormatter; 10 | pub use parameters::int::{IntParameter, IntRange, IntFormatter}; 11 | pub use parameters::map::ParameterMap; 12 | pub use parameters::parameter::Parameter; 13 | pub use parameters::range::ParameterRange; 14 | pub use plugin::Plugin; 15 | pub use processor::{Processor, ProcessorConfig, ProcessState, ProcessMode}; 16 | pub use transport::Transport; 17 | 18 | #[cfg(target_os="macos")] 19 | pub use formats::auv3; 20 | 21 | // Re-exports 22 | pub use log; 23 | pub use plinth_core; 24 | pub use raw_window_handle; 25 | pub use xxhash_rust; 26 | 27 | mod editor; 28 | pub mod error; 29 | mod event; 30 | mod host; 31 | mod formats; 32 | pub mod parameters; 33 | mod plugin; 34 | mod processor; 35 | pub mod string; 36 | mod transport; 37 | mod window_handle; 38 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters.rs: -------------------------------------------------------------------------------- 1 | pub mod bool; 2 | pub mod enums; 3 | pub mod float; 4 | pub mod formatter; 5 | pub mod group; 6 | pub mod info; 7 | pub mod int; 8 | pub mod kind; 9 | pub mod map; 10 | pub mod parameter; 11 | pub mod range; 12 | 13 | pub type ParameterId = u32; 14 | pub type ParameterValue = f64; 15 | 16 | pub type ModulationChangedCallback = Arc; 17 | 18 | use std::any::Any; 19 | use std::{collections::HashSet, sync::Arc}; 20 | 21 | use parameter::{Parameter, ParameterPlain}; 22 | 23 | use crate::error::Error; 24 | use crate::Event; 25 | 26 | pub fn has_duplicates(ids: &[ParameterId]) -> bool { 27 | let mut set = HashSet::new(); 28 | ids.iter().any(|id| !set.insert(id)) 29 | } 30 | 31 | pub trait Parameters { 32 | fn ids(&self) -> &[ParameterId]; 33 | fn get(&self, id: impl Into) -> Option<&dyn Parameter>; 34 | 35 | fn typed(&self, id: impl Into) -> Option<&T> { 36 | self.get(id) 37 | .and_then(|parameter| { 38 | let any_parameter = parameter as &dyn Any; 39 | any_parameter.downcast_ref::() 40 | }) 41 | } 42 | 43 | fn value(&self, id: impl Into) -> T::Plain { 44 | self.typed::(id).unwrap().plain() 45 | } 46 | 47 | fn modulated_value(&self, id: impl Into) -> T::Plain { 48 | self.typed::(id).unwrap().modulated_plain() 49 | } 50 | 51 | fn process_event(&self, event: &Event) { 52 | match event { 53 | Event::ParameterValue { id, value, .. } => { 54 | let parameter = self.get(*id).unwrap_or_else(|| panic!("Tried to get parameter with id {id} but it doesn't exist")); 55 | parameter.set_normalized_value(*value).unwrap(); 56 | }, 57 | 58 | Event::ParameterModulation { id, amount, .. } => { 59 | let parameter = self.get(*id).unwrap_or_else(|| panic!("Tried to get parameter with id {id} but it doesn't exist")); 60 | parameter.set_normalized_modulation(*amount); 61 | }, 62 | 63 | _ => {}, 64 | } 65 | } 66 | 67 | fn reset(&self) { 68 | for id in self.ids().iter().copied() { 69 | let parameter = self.get(id).unwrap(); 70 | parameter.set_normalized_value(parameter.info().default_normalized_value()).unwrap(); 71 | } 72 | } 73 | 74 | fn serialize(&self) -> impl Iterator { 75 | self.ids().iter() 76 | .map(|&id| { 77 | let parameter = self.get(id); 78 | (id, parameter.unwrap().serialize_value()) 79 | }) 80 | } 81 | 82 | fn deserialize(&self, parameters: impl IntoIterator) -> Result<(), Error> { 83 | self.reset(); 84 | 85 | for (id, value) in parameters.into_iter() { 86 | let parameter = match self.get(id) { 87 | Some(id) => id, 88 | None => { 89 | return Err(Error::ParameterIdError); 90 | }, 91 | }; 92 | 93 | parameter.deserialize_value(value)?; 94 | } 95 | 96 | Ok(()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/bool.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, sync::{atomic::AtomicBool, Arc}}; 2 | 3 | use portable_atomic::{AtomicF64, Ordering}; 4 | 5 | use crate::{error::Error, Parameter, ParameterFormatter, ParameterId, ParameterValue}; 6 | 7 | use super::{info::ParameterInfo, parameter::ParameterPlain, ModulationChangedCallback}; 8 | 9 | const DEFAULT_FALSE_STRING: &str = "False"; 10 | const DEFAULT_TRUE_STRING: &str = "True"; 11 | 12 | pub type ValueChangedCallback = Arc; 13 | 14 | pub struct BoolParameter { 15 | info: ParameterInfo, 16 | 17 | value: AtomicBool, 18 | normalized_modulation: AtomicF64, 19 | 20 | formatter: Arc>, 21 | 22 | value_changed: Option, 23 | modulation_changed: Option, 24 | } 25 | 26 | impl BoolParameter { 27 | pub fn new(id: impl Into, name: impl Into) -> Self { 28 | let info = ParameterInfo::new(id.into(), name.into()) 29 | .with_steps(1); 30 | 31 | Self { 32 | info, 33 | value: false.into(), 34 | normalized_modulation: 0.0.into(), 35 | formatter: Arc::new(BoolFormatter::new(DEFAULT_FALSE_STRING, DEFAULT_TRUE_STRING)), 36 | value_changed: None, 37 | modulation_changed: None, 38 | } 39 | } 40 | 41 | pub fn with_path(mut self, path: String) -> Self { 42 | self.info = self.info.with_path(path); 43 | self 44 | } 45 | 46 | pub fn with_default_value(mut self, default_value: bool) -> Self { 47 | let default_normalized_value = if default_value { 1.0 } else { 0.0 }; 48 | 49 | self.info = self.info.with_default_normalized_value(default_normalized_value); 50 | self.value.store(default_value, Ordering::Release); 51 | self 52 | } 53 | 54 | pub fn with_formatter(mut self, formatter: Arc>) -> Self { 55 | self.formatter = formatter; 56 | self 57 | } 58 | 59 | pub fn on_value_changed(mut self, value_changed: ValueChangedCallback) -> Self { 60 | self.value_changed = Some(value_changed); 61 | self 62 | } 63 | 64 | pub fn on_modulation_changed(mut self, modulation_changed: ModulationChangedCallback) -> Self { 65 | self.modulation_changed = Some(modulation_changed); 66 | self 67 | } 68 | 69 | pub fn as_bypass(mut self) -> Self { 70 | self.info = self.info.as_bypass(); 71 | self 72 | } 73 | 74 | pub fn set_value(&self, value: bool) { 75 | self.value.store(value, Ordering::Release); 76 | 77 | if let Some(on_value_changed) = self.value_changed.as_ref() { 78 | on_value_changed(self.info.id(), self.plain()); 79 | } 80 | } 81 | 82 | pub fn default_value(&self) -> bool { 83 | self.normalized_to_plain(self.info.default_normalized_value()) 84 | } 85 | } 86 | 87 | impl Clone for BoolParameter { 88 | fn clone(&self) -> Self { 89 | Self { 90 | info: self.info.clone(), 91 | 92 | value: self.value.load(Ordering::Acquire).into(), 93 | normalized_modulation: self.normalized_modulation.load(Ordering::Acquire).into(), 94 | 95 | formatter: self.formatter.clone(), 96 | 97 | value_changed: self.value_changed.clone(), 98 | modulation_changed: self.modulation_changed.clone(), 99 | } 100 | } 101 | } 102 | 103 | impl Display for BoolParameter { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | f.write_str(&self.formatter.value_to_string(self.plain())) 106 | } 107 | } 108 | 109 | impl Parameter for BoolParameter { 110 | fn info(&self) -> &ParameterInfo { 111 | &self.info 112 | } 113 | 114 | fn normalized_value(&self) -> ParameterValue { 115 | self.plain_to_normalized(self.value.load(Ordering::Acquire)) 116 | } 117 | 118 | fn set_normalized_value(&self, normalized: ParameterValue) -> Result<(), Error> { 119 | let normalized = f64::clamp(normalized, 0.0, 1.0); 120 | self.set_value(self.normalized_to_plain(normalized)); 121 | Ok(()) 122 | } 123 | 124 | fn normalized_modulation(&self) -> ParameterValue { 125 | self.normalized_modulation.load(Ordering::Acquire) 126 | } 127 | 128 | fn set_normalized_modulation(&self, amount: ParameterValue) { 129 | self.normalized_modulation.store(amount, Ordering::Release); 130 | 131 | if let Some(on_modulated_value_changed) = self.modulation_changed.as_ref() { 132 | on_modulated_value_changed(self.info.id(), self.normalized_modulation()); 133 | } 134 | } 135 | 136 | fn normalized_to_string(&self, value: ParameterValue) -> String { 137 | let value = self.normalized_to_plain(value); 138 | self.formatter.value_to_string(value) 139 | } 140 | 141 | fn string_to_normalized(&self, string: &str) -> Option { 142 | let plain = self.formatter.string_to_value(string)?; 143 | Some(self.plain_to_normalized(plain)) 144 | } 145 | 146 | fn serialize_value(&self) -> ParameterValue { 147 | self.normalized_value() 148 | } 149 | 150 | fn deserialize_value(&self, value: ParameterValue) -> Result<(), Error> { 151 | self.set_normalized_value(value) 152 | } 153 | } 154 | 155 | impl ParameterPlain for BoolParameter { 156 | type Plain = bool; 157 | 158 | fn normalized_to_plain(&self, value: ParameterValue) -> bool { 159 | value >= 0.5 160 | } 161 | 162 | fn plain_to_normalized(&self, plain: bool) -> ParameterValue { 163 | if plain { 164 | 1.0 165 | } else { 166 | 0.0 167 | } 168 | } 169 | } 170 | 171 | pub struct BoolFormatter { 172 | false_string: &'static str, 173 | true_string: &'static str, 174 | } 175 | 176 | impl BoolFormatter { 177 | pub const fn new(false_string: &'static str, true_string: &'static str) -> Self { 178 | Self { 179 | false_string, 180 | true_string, 181 | } 182 | } 183 | } 184 | 185 | impl ParameterFormatter for BoolFormatter { 186 | fn value_to_string(&self, value: bool) -> String { 187 | if value { 188 | self.true_string.to_string() 189 | } else { 190 | self.false_string.to_string() 191 | } 192 | } 193 | 194 | fn string_to_value(&self, string: &str) -> Option { 195 | let string = string.to_lowercase(); 196 | 197 | if string == self.false_string.to_lowercase() { 198 | Some(false) 199 | } else if string == self.true_string.to_lowercase() { 200 | Some(true) 201 | } else { 202 | None 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/formatter.rs: -------------------------------------------------------------------------------- 1 | pub trait ParameterFormatter : Send + Sync { 2 | fn value_to_string(&self, value: T) -> String; 3 | fn string_to_value(&self, string: &str) -> Option; 4 | } 5 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/group.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::Parameters; 4 | 5 | pub type ParameterGroupRef = Rc; 6 | 7 | pub fn from_parameters(parameters: &impl Parameters) -> Vec { 8 | let mut groups = Vec::new(); 9 | 10 | for &id in parameters.ids().iter() { 11 | let parameter = parameters.get(id).unwrap(); 12 | let path = parameter.info().path(); 13 | if path.is_empty() { 14 | continue; 15 | } 16 | 17 | // Create all units for the path 18 | let sub_paths: Vec<_> = path.split('/').collect(); 19 | let mut parameter_groups: Vec<_> = sub_paths 20 | .iter() 21 | .map(|sub_path| ParameterGroup::from_path(sub_path.to_string())).collect(); 22 | 23 | let mut parent = None; 24 | 25 | for mut group in parameter_groups.drain(..) { 26 | group.parent = parent.clone(); 27 | 28 | // Add unit to state if it doesn't exist already 29 | let group_ref = Rc::new(group); 30 | parent = Some(group_ref.clone()); 31 | 32 | if !groups.contains(&group_ref) { 33 | groups.push(group_ref); 34 | } 35 | } 36 | } 37 | 38 | groups 39 | } 40 | 41 | pub struct ParameterGroup { 42 | pub path: String, 43 | pub name: String, 44 | pub parent: Option, 45 | } 46 | 47 | impl PartialEq for ParameterGroup { 48 | fn eq(&self, other: &Self) -> bool { 49 | self.path == other.path 50 | } 51 | } 52 | 53 | impl ParameterGroup { 54 | pub fn from_path(path: String) -> Self { 55 | let name = path.split_once('/') 56 | .map(|(head, _)| head) 57 | .unwrap_or(&path) 58 | .to_string(); 59 | 60 | Self { 61 | path, 62 | name, 63 | parent: None, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/info.rs: -------------------------------------------------------------------------------- 1 | use crate::ParameterId; 2 | 3 | use super::ParameterValue; 4 | 5 | #[derive(Clone)] 6 | pub struct ParameterInfo { 7 | id: ParameterId, 8 | name: String, 9 | path: String, 10 | default_normalized_value: ParameterValue, 11 | steps: usize, 12 | is_bypass: bool, 13 | } 14 | 15 | impl ParameterInfo { 16 | pub fn new(id: ParameterId, name: String) -> Self { 17 | Self { 18 | id, 19 | name, 20 | path: Default::default(), 21 | default_normalized_value: Default::default(), 22 | steps: 0, 23 | is_bypass: false, 24 | } 25 | } 26 | 27 | pub fn with_path(mut self, path: String) -> Self { 28 | self.path = path; 29 | self 30 | } 31 | 32 | pub fn with_default_normalized_value(mut self, value: ParameterValue) -> Self { 33 | self.default_normalized_value = value; 34 | self 35 | } 36 | 37 | pub fn with_steps(mut self, steps: usize) -> Self { 38 | self.steps = steps; 39 | self 40 | } 41 | 42 | pub fn as_bypass(mut self) -> Self { 43 | self.is_bypass = true; 44 | self 45 | } 46 | 47 | pub fn id(&self) -> ParameterId { 48 | self.id 49 | } 50 | 51 | pub fn name(&self) -> &str { 52 | &self.name 53 | } 54 | 55 | pub fn path(&self) -> &str { 56 | &self.path 57 | } 58 | 59 | pub fn default_normalized_value(&self) -> ParameterValue { 60 | self.default_normalized_value 61 | } 62 | 63 | pub fn steps(&self) -> usize { 64 | self.steps 65 | } 66 | 67 | pub fn is_bypass(&self) -> bool { 68 | self.is_bypass 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/kind.rs: -------------------------------------------------------------------------------- 1 | use crate::ParameterId; 2 | 3 | pub trait ParameterKind : Into { 4 | } 5 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/map.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use crate::{Parameter, ParameterId, Parameters}; 4 | 5 | #[derive(Clone, Default)] 6 | pub struct ParameterMap { 7 | ids: Vec, 8 | map: HashMap>, 9 | } 10 | 11 | impl ParameterMap { 12 | pub fn new() -> Self { 13 | Self::default() 14 | } 15 | 16 | pub fn add(&mut self, parameter: impl Parameter) { 17 | let id = parameter.info().id(); 18 | assert!(!self.map.contains_key(&id), 19 | "Duplicate parameter id {id}. Old parameter was \"{}\", new parameter is \"{}\"", 20 | self.map.get(&id).unwrap().info().name(), 21 | parameter.info().name(), 22 | ); 23 | 24 | self.ids.push(id); 25 | self.map.insert(id, Arc::new(parameter)); 26 | } 27 | 28 | pub fn index_of(&self, parameter_id: impl Into) -> Option { 29 | let parameter_id = parameter_id.into(); 30 | self.ids.iter().position(|&id| id == parameter_id) 31 | } 32 | } 33 | 34 | impl Parameters for ParameterMap { 35 | fn ids(&self) -> &[ParameterId] { 36 | &self.ids 37 | } 38 | 39 | fn get(&self, id: impl Into) -> Option<&dyn Parameter> { 40 | self.map.get(&id.into()) 41 | .map(|parameter| parameter.as_ref()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/parameter.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use crate::error::Error; 4 | 5 | use super::{info::ParameterInfo, ParameterValue}; 6 | 7 | pub trait Parameter : Any + Send + Sync { 8 | fn info(&self) -> &ParameterInfo; 9 | 10 | fn normalized_value(&self) -> ParameterValue; 11 | fn set_normalized_value(&self, normalized: ParameterValue) -> Result<(), Error> ; 12 | 13 | fn normalized_modulation(&self) -> ParameterValue; 14 | fn set_normalized_modulation(&self, amount: ParameterValue); 15 | 16 | fn normalized_to_string(&self, value: ParameterValue) -> String; 17 | fn string_to_normalized(&self, string: &str) -> Option; 18 | 19 | fn serialize_value(&self) -> ParameterValue; 20 | fn deserialize_value(&self, value: ParameterValue) -> Result<(), Error> ; 21 | } 22 | 23 | pub trait ParameterPlain : Parameter { 24 | type Plain; 25 | 26 | fn normalized_to_plain(&self, normalized: ParameterValue) -> Self::Plain; 27 | fn plain_to_normalized(&self, plain: Self::Plain) -> ParameterValue; 28 | 29 | fn plain(&self) -> Self::Plain { 30 | self.normalized_to_plain(self.normalized_value()) 31 | } 32 | 33 | fn modulated_plain(&self) -> Self::Plain { 34 | self.normalized_to_plain(self.normalized_value() + self.normalized_modulation()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /plinth-plugin/src/parameters/range.rs: -------------------------------------------------------------------------------- 1 | use crate::parameters::ParameterValue; 2 | 3 | pub trait ParameterRange: Send + Sync { 4 | fn clamp(&self, value: T) -> T; 5 | fn steps(&self) -> usize; 6 | fn plain_to_normalized(&self, plain: T) -> Option; 7 | fn normalized_to_plain(&self, normalized: ParameterValue) -> T; 8 | } 9 | -------------------------------------------------------------------------------- /plinth-plugin/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::{io::{Read, Write}, rc::Rc}; 2 | 3 | use crate::{error::Error, host::HostInfo, processor::ProcessorConfig, Editor, Event, Host, Parameters, Processor}; 4 | 5 | pub trait Plugin { 6 | const NAME: &'static str; 7 | const VENDOR: &'static str; 8 | const VERSION: &'static str; 9 | 10 | const URL: Option<&'static str> = None; 11 | 12 | const HAS_AUX_INPUT: bool = false; 13 | const HAS_NOTE_INPUT: bool = false; 14 | const HAS_NOTE_OUTPUT: bool = false; 15 | 16 | type Processor: Processor; 17 | type Editor: Editor; 18 | type Parameters: Parameters; 19 | 20 | fn new(host_info: HostInfo) -> Self; 21 | 22 | fn with_parameters(&self, f: impl FnMut(&Self::Parameters) -> T) -> T; 23 | fn process_event(&mut self, event: &Event); 24 | 25 | fn create_processor(&mut self, config: ProcessorConfig) -> Self::Processor; 26 | fn create_editor(&mut self, host: Rc) -> Self::Editor; 27 | 28 | fn save_state(&self, writer: &mut impl Write) -> Result<(), Error>; 29 | fn load_state(&mut self, reader: &mut impl Read) -> Result<(), Error>; 30 | 31 | fn latency(&self) -> u32 { 32 | 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plinth-plugin/src/processor.rs: -------------------------------------------------------------------------------- 1 | use plinth_core::signals::signal::{Signal, SignalMut}; 2 | 3 | use crate::{event::Event, transport::Transport}; 4 | 5 | #[derive(Clone, Default)] 6 | pub struct ProcessorConfig { 7 | pub sample_rate: f64, 8 | pub min_block_size: usize, 9 | pub max_block_size: usize, 10 | pub process_mode: ProcessMode, 11 | } 12 | 13 | #[derive(Clone, Copy, Default, PartialEq)] 14 | pub enum ProcessMode { 15 | #[default] 16 | Realtime, 17 | Offline, 18 | } 19 | 20 | pub enum ProcessState { 21 | Error, 22 | Normal, 23 | Tail(usize), 24 | KeepAlive, 25 | } 26 | 27 | pub trait Processor: Send { 28 | fn reset(&mut self); 29 | fn process(&mut self, buffer: &mut impl SignalMut, aux: Option<&impl Signal>, transport: Option, events: impl Iterator) -> ProcessState; 30 | // Called when there's no audio to process 31 | fn process_events(&mut self, events: impl Iterator); 32 | } 33 | -------------------------------------------------------------------------------- /plinth-plugin/src/string.rs: -------------------------------------------------------------------------------- 1 | use std::iter::zip; 2 | 3 | use vst3::Steinberg::{char16, char8}; 4 | use widestring::U16CStr; 5 | 6 | pub fn char16_to_string(source: &[char16]) -> Option { 7 | let Ok(cstr) = U16CStr::from_slice_truncate(unsafe { std::mem::transmute::<&[i16], &[u16]>(source) }) else { 8 | return None; 9 | }; 10 | 11 | cstr.to_string().ok() 12 | } 13 | 14 | pub fn copy_u128_to_char8(source: &u128, target: &mut [char8; 16]) { 15 | target.fill(0); 16 | 17 | let source_bytes = source.to_be_bytes(); 18 | 19 | for (source_byte, target_byte) in zip(source_bytes, target) { 20 | *target_byte = source_byte as _; 21 | } 22 | } 23 | 24 | pub fn copy_str_to_char8(source: &str, target: &mut [char8]) { 25 | target.fill(0); 26 | 27 | let source_bytes = source.as_bytes(); 28 | 29 | // Account for null terminator 30 | assert!(target.len() > source_bytes.len()); 31 | 32 | for (source_byte, target_byte) in zip(source_bytes, target) { 33 | *target_byte = *source_byte as _; 34 | } 35 | } 36 | 37 | pub fn copy_str_to_char16(source: &str, target: &mut [char16]) { 38 | target.fill(0); 39 | 40 | let source_wide: Vec<_> = source.encode_utf16().collect(); 41 | 42 | // Account for null terminator 43 | assert!(target.len() > source_wide.len()); 44 | 45 | for (source_char, target_char) in zip(source_wide, target) { 46 | *target_char = source_char as _; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plinth-plugin/src/transport.rs: -------------------------------------------------------------------------------- 1 | pub struct Transport { 2 | pub(crate) playing: bool, 3 | pub(crate) tempo: f64, 4 | pub(crate) position_samples: i64, 5 | } 6 | 7 | impl Transport { 8 | pub fn new(playing: bool, tempo: f64, position_samples: i64) -> Self { 9 | Self { 10 | playing, 11 | tempo, 12 | position_samples, 13 | } 14 | } 15 | 16 | pub fn playing(&self) -> bool { 17 | self.playing 18 | } 19 | 20 | pub fn tempo(&self) -> f64 { 21 | self.tempo 22 | } 23 | 24 | pub fn position_samples(&self) -> i64 { 25 | self.position_samples 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plinth-plugin/src/window_handle.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use raw_window_handle::RawWindowHandle; 4 | 5 | #[cfg(target_os="linux")] 6 | pub fn from_ptr(parent: *mut c_void) -> RawWindowHandle { 7 | let raw_window_handle = raw_window_handle::XlibWindowHandle::new(parent as _); 8 | RawWindowHandle::Xlib(raw_window_handle) 9 | } 10 | 11 | #[cfg(target_os="macos")] 12 | pub fn from_ptr(parent: *mut c_void) -> RawWindowHandle { 13 | use std::ptr::NonNull; 14 | 15 | let raw_window_handle = raw_window_handle::AppKitWindowHandle::new( 16 | NonNull::new(parent as _).unwrap() 17 | ); 18 | RawWindowHandle::AppKit(raw_window_handle) 19 | } 20 | 21 | #[cfg(target_os="windows")] 22 | pub fn from_ptr(parent: *mut c_void) -> RawWindowHandle { 23 | let raw_window_handle = raw_window_handle::Win32WindowHandle::new(std::num::NonZeroIsize::new(parent as _).unwrap()); 24 | RawWindowHandle::Win32(raw_window_handle) 25 | } 26 | -------------------------------------------------------------------------------- /plugin-canvas-slint/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin-canvas-slint" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | authors = ["Jussi Viiri "] 7 | readme = "README.md" 8 | repository = "https://github.com/ilmai/plugin-things" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | cursor-icon.workspace = true 13 | i-slint-common.workspace = true 14 | i-slint-core.workspace = true 15 | i-slint-renderer-skia.workspace = true 16 | keyboard-types.workspace = true 17 | log.workspace = true 18 | plugin-canvas.workspace = true 19 | portable-atomic.workspace = true 20 | raw-window-handle.workspace = true 21 | slint.workspace = true 22 | -------------------------------------------------------------------------------- /plugin-canvas-slint/src/editor.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | use std::rc::{Rc, Weak}; 3 | 4 | use raw_window_handle::RawWindowHandle; 5 | use plugin_canvas::{event::EventResponse, window::WindowAttributes, Event}; 6 | use slint::platform::WindowAdapter; 7 | 8 | use crate::{platform::PluginCanvasPlatform, view::PluginView, window_adapter::{PluginCanvasWindowAdapter, WINDOW_ADAPTER_FROM_SLINT, WINDOW_TO_SLINT}}; 9 | 10 | pub struct SlintEditor; 11 | 12 | impl SlintEditor { 13 | pub fn open( 14 | parent: RawWindowHandle, 15 | window_attributes: WindowAttributes, 16 | view_builder: B 17 | ) -> Rc 18 | where 19 | C: PluginView + 'static, 20 | B: Fn(Rc) -> C + 'static, 21 | { 22 | let editor_handle = Rc::new(EditorHandle::new()); 23 | 24 | let window = plugin_canvas::Window::open( 25 | parent, 26 | window_attributes.clone(), 27 | { 28 | let editor_weak_ptr = Rc::downgrade(&editor_handle).into_raw(); 29 | let editor_thread = std::thread::current().id(); 30 | 31 | Box::new(move |event| { 32 | if std::thread::current().id() != editor_thread { 33 | log::warn!("Tried to call event callback from non-editor thread"); 34 | return EventResponse::Ignored; 35 | } 36 | 37 | let editor_weak = unsafe { Weak::from_raw(editor_weak_ptr) }; 38 | let response = if let Some(editor_handle) = editor_weak.upgrade() { 39 | editor_handle.on_event(&event) 40 | } else { 41 | EventResponse::Ignored 42 | }; 43 | 44 | // Leak the weak reference to avoid dropping it 45 | let _ = editor_weak.into_raw(); 46 | 47 | response 48 | }) 49 | }, 50 | ).unwrap(); 51 | 52 | // It's ok if this fails as it just means it has already been set 53 | slint::platform::set_platform(Box::new(PluginCanvasPlatform)).ok(); 54 | 55 | let window = Rc::new(window); 56 | WINDOW_TO_SLINT.set(Some(window.clone())); 57 | 58 | let view = view_builder(window); 59 | view.window().show().unwrap(); 60 | 61 | let window_adapter = WINDOW_ADAPTER_FROM_SLINT.take().unwrap(); 62 | window_adapter.set_view(Box::new(view)); 63 | 64 | editor_handle.set_window_adapter(window_adapter); 65 | editor_handle 66 | } 67 | } 68 | 69 | pub struct EditorHandle { 70 | window_adapter: OnceCell>, 71 | } 72 | 73 | impl EditorHandle { 74 | pub fn on_frame(&self) { 75 | self.on_event(&Event::Draw); 76 | } 77 | 78 | pub fn set_window_size(&self, width: f64, height: f64) { 79 | let size = slint::LogicalSize { 80 | width: width as _, 81 | height: height as _, 82 | }; 83 | 84 | if let Some(window_adapter) = self.window_adapter() { 85 | window_adapter.set_size(size.into()); 86 | } 87 | } 88 | 89 | pub fn set_scale(&self, scale: f64) { 90 | if let Some(window_adapter) = self.window_adapter() { 91 | window_adapter.set_scale(scale); 92 | } 93 | } 94 | 95 | fn new() -> Self { 96 | Self { 97 | window_adapter: Default::default(), 98 | } 99 | } 100 | 101 | fn window_adapter(&self) -> Option<&PluginCanvasWindowAdapter> { 102 | self.window_adapter.get().map(|adapter| &**adapter) 103 | } 104 | 105 | fn set_window_adapter(&self, window_adapter: Rc) { 106 | self.window_adapter.set(window_adapter).unwrap(); 107 | } 108 | 109 | fn on_event(&self, event: &Event) -> EventResponse { 110 | if let Some(window_adapter) = self.window_adapter() { 111 | window_adapter.on_event(event) 112 | } else { 113 | EventResponse::Ignored 114 | } 115 | } 116 | } 117 | 118 | impl Drop for EditorHandle { 119 | fn drop(&mut self) { 120 | if let Some(window_adapter) = self.window_adapter.get() { 121 | window_adapter.close(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /plugin-canvas-slint/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod editor; 2 | pub mod platform; 3 | pub mod view; 4 | pub mod window_adapter; 5 | 6 | // Re-exports 7 | pub use plugin_canvas; 8 | -------------------------------------------------------------------------------- /plugin-canvas-slint/src/platform.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use i_slint_core::{platform::{Platform, PlatformError}, window::WindowAdapter}; 4 | 5 | use crate::window_adapter::PluginCanvasWindowAdapter; 6 | 7 | pub struct PluginCanvasPlatform; 8 | 9 | impl Platform for PluginCanvasPlatform { 10 | fn create_window_adapter(&self) -> Result, PlatformError> { 11 | PluginCanvasWindowAdapter::new() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugin-canvas-slint/src/view.rs: -------------------------------------------------------------------------------- 1 | use plugin_canvas::{Event, event::EventResponse}; 2 | 3 | pub trait PluginView { 4 | fn window(&self) -> &slint::Window; 5 | fn on_event(&self, event: &Event) -> EventResponse; 6 | } 7 | -------------------------------------------------------------------------------- /plugin-canvas/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin-canvas" 3 | version = "0.0.0" 4 | edition = "2024" 5 | 6 | authors = ["Jussi Viiri "] 7 | readme = "README.md" 8 | repository = "https://github.com/ilmai/plugin-things" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | bitflags = "2.9" 13 | cursor-icon.workspace = true 14 | keyboard-types.workspace = true 15 | raw-window-handle.workspace = true 16 | uuid = { version = "1.4", features = ["fast-rng", "v4"] } 17 | 18 | [target.'cfg(target_os="linux")'.dependencies] 19 | sys-locale = "0.3" 20 | x11rb = { version = "0.13", features = ["allow-unsafe-code", "cursor", "render", "resource_manager", "xfixes", "xkb"] } 21 | xkbcommon = { version = "0.8", features = ["x11"] } 22 | 23 | [target.'cfg(target_os="macos")'.dependencies] 24 | objc2 = { version = "0.6", features = ["verify"] } 25 | objc2-app-kit = { version = "0.3", features = ["NSCursor", "NSDragging", "NSEvent", "NSPasteboard", "NSPasteboardItem", "NSScreen", "NSTrackingArea"] } 26 | objc2-core-foundation = { version = "0.3", features = ["CFCGTypes"] } 27 | objc2-core-graphics = { version = "0.3", features = ["CGError", "CGRemoteOperation"] } 28 | objc2-core-video = "0.3" 29 | objc2-foundation = { version = "0.3", features = ["NSOperation", "NSRunLoop"] } 30 | objc2-quartz-core = { version = "0.3", features = ["CADisplayLink"] } 31 | 32 | [target.'cfg(target_os="windows")'.dependencies] 33 | windows-core = "0.61" 34 | 35 | [target.'cfg(target_os="windows")'.dependencies.windows] 36 | version = "0.61" 37 | features = [ 38 | "Win32_Foundation", 39 | "Win32_Graphics_Dwm", 40 | "Win32_Graphics_Dxgi", 41 | "Win32_Graphics_Dxgi_Common", 42 | "Win32_Graphics_Gdi", 43 | "Win32_System_Com", 44 | "Win32_System_Com_StructuredStorage", 45 | "Win32_System_Ole", 46 | "Win32_System_SystemInformation", 47 | "Win32_System_SystemServices", 48 | "Win32_System_Threading", 49 | "Win32_UI_Controls", 50 | "Win32_UI_HiDpi", 51 | "Win32_UI_Input_KeyboardAndMouse", 52 | "Win32_UI_Shell", 53 | "Win32_UI_WindowsAndMessaging", 54 | ] 55 | -------------------------------------------------------------------------------- /plugin-canvas/README.md: -------------------------------------------------------------------------------- 1 | # plugin-canvas, an opinionated windowing abstraction crate for audio plugins 2 | 3 | Heavily inspired by the following crates: 4 | - baseview 5 | - display-link 6 | - glazier 7 | - winit 8 | -------------------------------------------------------------------------------- /plugin-canvas/src/dimensions.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | 3 | #[derive(Clone, Debug, Default)] 4 | pub struct LogicalPosition { 5 | pub x: f64, 6 | pub y: f64, 7 | } 8 | 9 | impl LogicalPosition { 10 | pub fn from_physical(position: &PhysicalPosition, scale: f64) -> Self { 11 | LogicalPosition { 12 | x: position.x as f64 / scale, 13 | y: position.y as f64 / scale, 14 | } 15 | } 16 | 17 | pub fn to_physical(&self, scale: f64) -> PhysicalPosition { 18 | PhysicalPosition::from_logical(self, scale) 19 | } 20 | } 21 | 22 | impl From<(T, T)> for LogicalPosition 23 | where 24 | f64: From, 25 | { 26 | fn from(value: (T, T)) -> Self { 27 | Self { 28 | x: value.0.into(), 29 | y: value.1.into(), 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct PhysicalPosition { 36 | pub x: i32, 37 | pub y: i32, 38 | } 39 | 40 | impl PhysicalPosition { 41 | pub fn from_logical(position: &LogicalPosition, scale: f64) -> Self { 42 | Self { 43 | x: (position.x * scale) as i32, 44 | y: (position.y * scale) as i32, 45 | } 46 | } 47 | 48 | pub fn to_logical(&self, scale: f64) -> LogicalPosition { 49 | LogicalPosition::from_physical(self, scale) 50 | } 51 | } 52 | 53 | impl From<(T, T)> for PhysicalPosition 54 | where 55 | i32: From, 56 | { 57 | fn from(value: (T, T)) -> Self { 58 | Self { 59 | x: value.0.into(), 60 | y: value.1.into(), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Clone, Copy, Debug)] 66 | pub struct LogicalSize { 67 | pub width: f64, 68 | pub height: f64, 69 | } 70 | 71 | impl LogicalSize { 72 | pub fn new(width: f64, height: f64) -> Self { 73 | LogicalSize { width, height } 74 | } 75 | 76 | pub fn from_physical(size: &PhysicalSize, scale: f64) -> Self { 77 | Self { 78 | width: size.width as f64 / scale, 79 | height: size.height as f64 / scale, 80 | } 81 | } 82 | 83 | pub fn to_physical(&self, scale: f64) -> PhysicalSize { 84 | PhysicalSize::from_logical(self, scale) 85 | } 86 | } 87 | 88 | impl From<(T, T)> for LogicalSize 89 | where 90 | f64: From, 91 | { 92 | fn from(value: (T, T)) -> Self { 93 | Self { 94 | width: value.0.into(), 95 | height: value.1.into(), 96 | } 97 | } 98 | } 99 | 100 | impl Mul for LogicalSize { 101 | type Output = LogicalSize; 102 | 103 | fn mul(self, rhs: f64) -> Self::Output { 104 | LogicalSize { 105 | width: self.width * rhs, 106 | height: self.height * rhs, 107 | } 108 | } 109 | } 110 | 111 | #[derive(Clone, Copy, Debug)] 112 | pub struct PhysicalSize { 113 | pub width: u32, 114 | pub height: u32, 115 | } 116 | 117 | impl PhysicalSize { 118 | pub fn new(width: u32, height: u32) -> Self { 119 | Self { 120 | width, 121 | height, 122 | } 123 | } 124 | 125 | pub fn from_logical(size: &LogicalSize, scale: f64) -> Self { 126 | Self { 127 | width: (size.width * scale).round() as u32, 128 | height: (size.height * scale).round() as u32, 129 | } 130 | } 131 | 132 | pub fn to_logical(&self, scale: f64) -> LogicalSize { 133 | LogicalSize::from_physical(self, scale) 134 | } 135 | } 136 | 137 | impl From<(T, T)> for PhysicalSize 138 | where 139 | u32: From, 140 | { 141 | fn from(value: (T, T)) -> Self { 142 | Self { 143 | width: value.0.into(), 144 | height: value.1.into(), 145 | } 146 | } 147 | } 148 | 149 | #[derive(Clone, Debug)] 150 | pub struct Size { 151 | logical_size: LogicalSize, 152 | physical_size: PhysicalSize, 153 | scale: f64, 154 | } 155 | 156 | impl Size { 157 | pub fn with_logical_size(logical_size: LogicalSize, scale: f64) -> Self { 158 | Self { 159 | logical_size, 160 | physical_size: PhysicalSize::from_logical(&logical_size, scale), 161 | scale, 162 | } 163 | } 164 | 165 | pub fn with_physical_size(physical_size: PhysicalSize, scale: f64) -> Self { 166 | Self { 167 | logical_size: LogicalSize::from_physical(&physical_size, scale), 168 | physical_size, 169 | scale, 170 | } 171 | } 172 | 173 | pub fn logical_size(&self) -> &LogicalSize { 174 | &self.logical_size 175 | } 176 | 177 | pub fn physical_size(&self) -> &PhysicalSize { 178 | &self.physical_size 179 | } 180 | 181 | pub fn scale(&self) -> f64 { 182 | self.scale 183 | } 184 | 185 | pub fn set_scale(&mut self, scale: f64) { 186 | self.scale = scale; 187 | self.physical_size = PhysicalSize::from_logical(&self.logical_size, scale); 188 | } 189 | 190 | pub fn scale_by(&mut self, scale: f64) { 191 | self.set_scale(self.scale * scale); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /plugin-canvas/src/drag_drop.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq)] 4 | pub enum DropOperation { 5 | None, 6 | Copy, 7 | Move, 8 | Link, 9 | } 10 | 11 | #[derive(Clone, Debug, Default)] 12 | pub enum DropData { 13 | #[default] 14 | None, 15 | Files(Vec), 16 | } 17 | -------------------------------------------------------------------------------- /plugin-canvas/src/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os="linux")] 2 | use x11rb::x11_utils; 3 | 4 | #[derive(Debug)] 5 | pub enum Error { 6 | PlatformError(String), 7 | #[cfg(target_os="linux")] 8 | X11ConnectError(x11rb::errors::ConnectError), 9 | #[cfg(target_os="linux")] 10 | X11ConnectionError(x11rb::errors::ConnectionError), 11 | #[cfg(target_os="linux")] 12 | X11Error(x11_utils::X11Error), 13 | #[cfg(target_os="linux")] 14 | X11IdsExhausted, 15 | #[cfg(target_os="windows")] 16 | WindowsError(windows::core::Error), 17 | } 18 | 19 | #[cfg(target_os="linux")] 20 | impl From for Error { 21 | fn from(error: x11rb::errors::ConnectError) -> Self { 22 | Self::X11ConnectError(error) 23 | } 24 | } 25 | 26 | #[cfg(target_os="linux")] 27 | impl From for Error { 28 | fn from(error: x11rb::errors::ConnectionError) -> Self { 29 | Self::X11ConnectionError(error) 30 | } 31 | } 32 | 33 | #[cfg(target_os="linux")] 34 | impl From for Error { 35 | fn from(error: x11rb::errors::ReplyOrIdError) -> Self { 36 | match error { 37 | x11rb::errors::ReplyOrIdError::IdsExhausted => Self::X11IdsExhausted, 38 | x11rb::errors::ReplyOrIdError::ConnectionError(error) => Self::X11ConnectionError(error), 39 | x11rb::errors::ReplyOrIdError::X11Error(error) => Self::X11Error(error), 40 | } 41 | } 42 | } 43 | 44 | #[cfg(target_os="windows")] 45 | impl From for Error { 46 | fn from(error: windows::core::Error) -> Self { 47 | Self::WindowsError(error) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugin-canvas/src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::{dimensions::LogicalPosition, drag_drop::{DropData, DropOperation}, keyboard::KeyboardModifiers}; 2 | 3 | #[derive(Clone, Copy, Debug)] 4 | pub enum MouseButton { 5 | Left, 6 | Right, 7 | Middle, 8 | } 9 | 10 | #[derive(Clone, Debug)] 11 | pub enum Event { 12 | Draw, 13 | 14 | KeyDown { 15 | key_code: keyboard_types::Code, 16 | text: Option, 17 | }, 18 | 19 | KeyUp { 20 | key_code: keyboard_types::Code, 21 | text: Option, 22 | }, 23 | 24 | KeyboardModifiers { 25 | modifiers: KeyboardModifiers, 26 | }, 27 | 28 | MouseButtonDown { 29 | button: MouseButton, 30 | position: LogicalPosition, 31 | }, 32 | 33 | MouseButtonUp { 34 | button: MouseButton, 35 | position: LogicalPosition, 36 | }, 37 | 38 | MouseExited, 39 | 40 | MouseMoved { 41 | position: LogicalPosition, 42 | }, 43 | 44 | MouseWheel { 45 | position: LogicalPosition, 46 | delta_x: f64, 47 | delta_y: f64, 48 | }, 49 | 50 | DragEntered { 51 | position: LogicalPosition, 52 | data: DropData, 53 | }, 54 | 55 | DragExited, 56 | 57 | DragMoved { 58 | position: LogicalPosition, 59 | data: DropData, 60 | }, 61 | 62 | DragDropped { 63 | position: LogicalPosition, 64 | data: DropData, 65 | }, 66 | } 67 | 68 | #[derive(Clone, Copy, Debug, PartialEq)] 69 | pub enum EventResponse { 70 | Handled, 71 | Ignored, 72 | DropAccepted(DropOperation), 73 | } 74 | 75 | pub type EventCallback = dyn Fn(Event) -> EventResponse; 76 | -------------------------------------------------------------------------------- /plugin-canvas/src/keyboard.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 5 | pub struct KeyboardModifiers: u32 { 6 | const Alt = 0b0001; 7 | const Control = 0b0010; 8 | const Meta = 0b0100; 9 | const Shift = 0b1000; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /plugin-canvas/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod dimensions; 2 | pub mod drag_drop; 3 | pub mod error; 4 | pub mod event; 5 | pub mod keyboard; 6 | pub mod window; 7 | 8 | pub use dimensions::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; 9 | pub use event::{Event, MouseButton}; 10 | pub use window::Window; 11 | 12 | mod platform; 13 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform.rs: -------------------------------------------------------------------------------- 1 | pub mod interface; 2 | pub mod os_window_handle; 3 | 4 | #[cfg(target_os="linux")] 5 | pub mod x11; 6 | #[cfg(target_os="macos")] 7 | pub mod mac; 8 | #[cfg(target_os="windows")] 9 | pub mod win32; 10 | 11 | #[cfg(target_os="linux")] 12 | pub use x11::*; 13 | #[cfg(target_os="macos")] 14 | pub use mac::*; 15 | #[cfg(target_os="windows")] 16 | pub use win32::*; 17 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/interface.rs: -------------------------------------------------------------------------------- 1 | use cursor_icon::CursorIcon; 2 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; 3 | 4 | use crate::{error::Error, event::EventCallback, window::WindowAttributes, LogicalPosition, LogicalSize}; 5 | 6 | use super::os_window_handle::OsWindowHandle; 7 | 8 | pub(crate) trait OsWindowInterface: HasDisplayHandle + HasWindowHandle + Sized { 9 | fn open( 10 | parent_window_handle: RawWindowHandle, 11 | window_attributes: WindowAttributes, 12 | event_callback: Box, 13 | ) -> Result; 14 | 15 | fn os_scale(&self) -> f64; 16 | 17 | fn resized(&self, size: LogicalSize); 18 | 19 | fn set_cursor(&self, cursor: Option); 20 | fn set_input_focus(&self, focus: bool); 21 | fn warp_mouse(&self, position: LogicalPosition); 22 | 23 | fn poll_events(&self) -> Result<(), Error>; 24 | } 25 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/mac.rs: -------------------------------------------------------------------------------- 1 | pub mod keyboard; 2 | pub mod view; 3 | pub mod window; 4 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/mac/keyboard.rs: -------------------------------------------------------------------------------- 1 | use keyboard_types::Code; 2 | 3 | pub fn key_event_to_keyboard_type_code(code: u16) -> Code { 4 | // Codes from Events.h 5 | match code { 6 | 0x00 => Code::KeyA, // kVK_ANSI_A 7 | 0x01 => Code::KeyS, // kVK_ANSI_S 8 | 0x02 => Code::KeyD, // kVK_ANSI_D 9 | 0x03 => Code::KeyF, // kVK_ANSI_F 10 | 0x04 => Code::KeyH, // kVK_ANSI_H 11 | 0x05 => Code::KeyG, // kVK_ANSI_G 12 | 0x06 => Code::KeyZ, // kVK_ANSI_Z 13 | 0x07 => Code::KeyX, // kVK_ANSI_X 14 | 0x08 => Code::KeyC, // kVK_ANSI_C 15 | 0x09 => Code::KeyV, // kVK_ANSI_V 16 | 0x0B => Code::KeyB, // kVK_ANSI_B 17 | 0x0C => Code::KeyQ, // kVK_ANSI_Q 18 | 0x0D => Code::KeyW, // kVK_ANSI_W 19 | 0x0E => Code::KeyE, // kVK_ANSI_E 20 | 0x0F => Code::KeyR, // kVK_ANSI_R 21 | 0x10 => Code::KeyY, // kVK_ANSI_Y 22 | 0x11 => Code::KeyT, // kVK_ANSI_T 23 | 0x12 => Code::Digit1, // kVK_ANSI_1 24 | 0x13 => Code::Digit2, // kVK_ANSI_2 25 | 0x14 => Code::Digit3, // kVK_ANSI_3 26 | 0x15 => Code::Digit4, // kVK_ANSI_4 27 | 0x16 => Code::Digit6, // kVK_ANSI_6 28 | 0x17 => Code::Digit5, // kVK_ANSI_5 29 | 0x18 => Code::Equal, // kVK_ANSI_Equal 30 | 0x19 => Code::Digit9, // kVK_ANSI_9 31 | 0x1A => Code::Digit7, // kVK_ANSI_7 32 | 0x1B => Code::Minus, // kVK_ANSI_Minus 33 | 0x1C => Code::Digit8, // kVK_ANSI_8 34 | 0x1D => Code::Digit0, // kVK_ANSI_0 35 | 0x1E => Code::BracketRight, // kVK_ANSI_RightBracket 36 | 0x1F => Code::KeyO, // kVK_ANSI_O 37 | 0x20 => Code::KeyU, // kVK_ANSI_U 38 | 0x21 => Code::BracketLeft, // kVK_ANSI_LeftBracket 39 | 0x22 => Code::KeyI, // kVK_ANSI_I 40 | 0x23 => Code::KeyP, // kVK_ANSI_P 41 | 0x24 => Code::Enter, // kVK_Return 42 | 0x25 => Code::KeyL, // kVK_ANSI_L 43 | 0x26 => Code::KeyJ, // kVK_ANSI_J 44 | 0x27 => Code::Quote, // kVK_ANSI_Quote 45 | 0x28 => Code::KeyK, // kVK_ANSI_K 46 | 0x29 => Code::Semicolon, // kVK_ANSI_Semicolon 47 | 0x2A => Code::Backslash, // kVK_ANSI_Backslash 48 | 0x2B => Code::Comma, // kVK_ANSI_Comma 49 | 0x2C => Code::Slash, // kVK_ANSI_Slash 50 | 0x2D => Code::KeyN, // kVK_ANSI_N 51 | 0x2E => Code::KeyM, // kVK_ANSI_M 52 | 0x2F => Code::Period, // kVK_ANSI_Period 53 | 0x30 => Code::Tab, // kVK_Tab 54 | 0x31 => Code::Space, // kVK_Space 55 | 0x32 => Code::Backquote, // kVK_ANSI_Grave 56 | 0x33 => Code::Backspace, // kVK_Delete 57 | 0x35 => Code::Escape, // kVK_Escape 58 | 0x36 => Code::MetaRight, // kVK_RightCommand 59 | 0x37 => Code::MetaLeft, // kVK_Command 60 | 0x38 => Code::ShiftLeft, // kVK_Shift 61 | 0x39 => Code::CapsLock, // kVK_CapsLock 62 | 0x3A => Code::AltLeft, // kVK_Option 63 | 0x3B => Code::ControlLeft, // kVK_Control 64 | 0x3C => Code::ShiftRight, // kVK_RightShift 65 | 0x3D => Code::AltRight, // kVK_RightOption 66 | 0x3E => Code::ControlRight, // kVK_RightControl 67 | 0x3F => Code::Fn, // kVK_Function 68 | 0x40 => Code::F17, // kVK_F17 69 | 0x41 => Code::NumpadDecimal, // kVK_ANSI_KeypadDecimal 70 | 0x43 => Code::NumpadMultiply, // kVK_ANSI_KeypadMultiply 71 | 0x45 => Code::NumpadAdd, // kVK_ANSI_KeypadPlus 72 | 0x47 => Code::NumpadClear, // kVK_ANSI_KeypadClear 73 | 0x48 => Code::AudioVolumeUp, // kVK_VolumeUp 74 | 0x49 => Code::AudioVolumeDown, // kVK_VolumeDown 75 | 0x4A => Code::AudioVolumeMute, // kVK_Mute 76 | 0x4B => Code::NumpadDivide, // kVK_ANSI_KeypadDivide 77 | 0x4C => Code::NumpadEnter, // kVK_ANSI_KeypadEnter 78 | 0x4E => Code::NumpadSubtract, // kVK_ANSI_KeypadMinus 79 | 0x4F => Code::F18, // kVK_F18 80 | 0x50 => Code::F19, // kVK_F19 81 | 0x51 => Code::NumpadEqual, // kVK_ANSI_KeypadEquals 82 | 0x52 => Code::Numpad0, // kVK_ANSI_Keypad0 83 | 0x53 => Code::Numpad1, // kVK_ANSI_Keypad1 84 | 0x54 => Code::Numpad2, // kVK_ANSI_Keypad2 85 | 0x55 => Code::Numpad3, // kVK_ANSI_Keypad3 86 | 0x56 => Code::Numpad4, // kVK_ANSI_Keypad4 87 | 0x57 => Code::Numpad5, // kVK_ANSI_Keypad5 88 | 0x58 => Code::Numpad6, // kVK_ANSI_Keypad6 89 | 0x59 => Code::Numpad7, // kVK_ANSI_Keypad7 90 | 0x5A => Code::F20, // kVK_F20 91 | 0x5B => Code::Numpad8, // kVK_ANSI_Keypad8 92 | 0x5C => Code::Numpad9, // kVK_ANSI_Keypad9 93 | 0x60 => Code::F5, // kVK_F5 94 | 0x61 => Code::F6, // kVK_F6 95 | 0x62 => Code::F7, // kVK_F7 96 | 0x63 => Code::F3, // kVK_F3 97 | 0x64 => Code::F8, // kVK_F8 98 | 0x65 => Code::F9, // kVK_F9 99 | 0x67 => Code::F11, // kVK_F11 100 | 0x69 => Code::F13, // kVK_F13 101 | 0x6A => Code::F16, // kVK_F16 102 | 0x6B => Code::F14, // kVK_F14 103 | 0x6D => Code::F10, // kVK_F10 104 | 0x6E => Code::ContextMenu, // kVK_ContextualMenu 105 | 0x6F => Code::F12, // kVK_F12 106 | 0x71 => Code::F15, // kVK_F15 107 | 0x72 => Code::Help, // kVK_Help 108 | 0x73 => Code::Home, // kVK_Home 109 | 0x74 => Code::PageUp, // kVK_PageUp 110 | 0x75 => Code::Delete, // kVK_ForwardDelete 111 | 0x76 => Code::F4, // kVK_F4 112 | 0x77 => Code::End, // kVK_End 113 | 0x78 => Code::F2, // kVK_F2 114 | 0x79 => Code::PageDown, // kVK_PageDown 115 | 0x7A => Code::F1, // kVK_F1 116 | 0x7B => Code::ArrowLeft, // kVK_LeftArrow 117 | 0x7C => Code::ArrowRight, // kVK_RightArrow 118 | 0x7D => Code::ArrowDown, // kVK_DownArrow 119 | 0x7E => Code::ArrowUp, // kVK_UpArrow 120 | 121 | _ => Code::Unidentified, 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/os_window_handle.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Deref, rc::Rc}; 2 | 3 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; 4 | 5 | use super::window::OsWindow; 6 | 7 | pub(crate) struct OsWindowHandle { 8 | os_window: Rc, 9 | } 10 | 11 | impl OsWindowHandle { 12 | pub(super) fn new(os_window: Rc) -> Self { 13 | Self { 14 | os_window, 15 | } 16 | } 17 | } 18 | 19 | impl Deref for OsWindowHandle { 20 | type Target = OsWindow; 21 | 22 | fn deref(&self) -> &Self::Target { 23 | self.os_window.as_ref() 24 | } 25 | } 26 | 27 | impl HasWindowHandle for OsWindowHandle { 28 | fn window_handle(&self) -> Result, raw_window_handle::HandleError> { 29 | self.os_window.as_ref().window_handle() 30 | } 31 | } 32 | 33 | impl HasDisplayHandle for OsWindowHandle { 34 | fn display_handle(&self) -> Result, raw_window_handle::HandleError> { 35 | self.os_window.as_ref().display_handle() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, str::FromStr, os::windows::prelude::OsStrExt}; 2 | 3 | use windows::Win32::{System::SystemServices::IMAGE_DOS_HEADER, Foundation::HINSTANCE, UI::WindowsAndMessaging::WM_USER}; 4 | 5 | pub mod cursors; 6 | pub mod drop_target; 7 | pub mod keyboard; 8 | pub mod message_window; 9 | pub mod version; 10 | pub mod window; 11 | 12 | unsafe extern "C" { 13 | static __ImageBase: IMAGE_DOS_HEADER; 14 | } 15 | 16 | thread_local! { 17 | static PLUGIN_HINSTANCE: HINSTANCE = unsafe { HINSTANCE(&__ImageBase as *const IMAGE_DOS_HEADER as _) }; 18 | } 19 | 20 | const WM_USER_FRAME_TIMER: u32 = WM_USER + 1; 21 | const WM_USER_CHAR: u32 = WM_USER + 2; 22 | const WM_USER_KEY_DOWN: u32 = WM_USER + 3; 23 | const WM_USER_KEY_UP: u32 = WM_USER + 4; 24 | 25 | fn to_wstr(string: impl AsRef) -> Vec { 26 | let mut wstr: Vec<_> = OsString::from_str(string.as_ref()).unwrap().encode_wide().collect(); 27 | wstr.push(0); 28 | wstr 29 | } 30 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32/cursors.rs: -------------------------------------------------------------------------------- 1 | use windows::core::PCWSTR; 2 | use windows::Win32::UI::WindowsAndMessaging::{HCURSOR, LoadImageW, IDC_ARROW, IDC_HAND, IMAGE_CURSOR, LR_SHARED, IDC_APPSTARTING, IDC_CROSS, IDC_HELP, IDC_IBEAM, IDC_SIZEALL, IDC_NO, IDC_SIZEWE, IDC_SIZENESW, IDC_SIZENWSE, IDC_WAIT, IDC_SIZENS}; 3 | 4 | pub struct Cursors { 5 | pub app_starting: HCURSOR, 6 | pub arrow: HCURSOR, 7 | pub cross: HCURSOR, 8 | pub hand: HCURSOR, 9 | pub help: HCURSOR, 10 | pub ibeam: HCURSOR, 11 | pub no: HCURSOR, 12 | pub size_all: HCURSOR, 13 | pub size_ns: HCURSOR, 14 | pub size_ew: HCURSOR, 15 | pub size_nesw: HCURSOR, 16 | pub size_nwse: HCURSOR, 17 | pub wait: HCURSOR, 18 | } 19 | 20 | impl Cursors { 21 | pub fn new() -> Self { 22 | Self { 23 | app_starting: Self::load_cursor(IDC_APPSTARTING), 24 | arrow: Self::load_cursor(IDC_ARROW), 25 | cross: Self::load_cursor(IDC_CROSS), 26 | hand: Self::load_cursor(IDC_HAND), 27 | help: Self::load_cursor(IDC_HELP), 28 | ibeam: Self::load_cursor(IDC_IBEAM), 29 | size_all: Self::load_cursor(IDC_SIZEALL), 30 | no: Self::load_cursor(IDC_NO), 31 | size_ns: Self::load_cursor(IDC_SIZENS), 32 | size_ew: Self::load_cursor(IDC_SIZEWE), 33 | size_nesw: Self::load_cursor(IDC_SIZENESW), 34 | size_nwse: Self::load_cursor(IDC_SIZENWSE), 35 | wait: Self::load_cursor(IDC_WAIT), 36 | } 37 | } 38 | 39 | fn load_cursor(name: PCWSTR) -> HCURSOR { 40 | let handle = unsafe { LoadImageW( 41 | None, 42 | name, 43 | IMAGE_CURSOR, 44 | 0, 45 | 0, 46 | LR_SHARED, 47 | ).unwrap() }; 48 | 49 | HCURSOR(handle.0) 50 | } 51 | } 52 | 53 | impl Default for Cursors { 54 | fn default() -> Self { 55 | Self::new() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32/drop_target.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::ffi::OsString; 3 | use std::ops::Deref; 4 | use std::os::windows::prelude::OsStringExt; 5 | use std::ptr::null_mut; 6 | use std::rc::Weak; 7 | 8 | use windows::Win32::Foundation::{POINTL, POINT}; 9 | use windows::Win32::Graphics::Gdi::MapWindowPoints; 10 | use windows::Win32::System::Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; 11 | use windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS; 12 | use windows::Win32::UI::Shell::{DragQueryFileW, HDROP}; 13 | use windows::core::implement; 14 | use windows::Win32::System::Ole::{IDropTarget, IDropTarget_Impl, DROPEFFECT, CF_HDROP, DROPEFFECT_NONE, DROPEFFECT_COPY, DROPEFFECT_MOVE, DROPEFFECT_LINK}; 15 | use windows::Win32::UI::WindowsAndMessaging::HWND_DESKTOP; 16 | 17 | use crate::event::EventResponse; 18 | use crate::platform::interface::OsWindowInterface; 19 | use crate::{LogicalPosition, PhysicalPosition}; 20 | use crate::drag_drop::{DropData, DropOperation}; 21 | use super::window::OsWindow; 22 | 23 | #[implement(IDropTarget)] 24 | pub(super) struct DropTarget { 25 | window: Weak, 26 | drop_data: RefCell, 27 | } 28 | 29 | impl DropTarget { 30 | pub fn new(window: Weak) -> Self { 31 | Self { 32 | window, 33 | drop_data: Default::default(), 34 | } 35 | } 36 | 37 | fn parse_drag_data(&self, pdataobj: windows_core::Ref<'_, IDataObject>) -> windows::core::Result<()> { 38 | let Some(data_object) = pdataobj.deref() else { 39 | *self.drop_data.borrow_mut() = DropData::None; 40 | return Ok(()); 41 | }; 42 | 43 | let format = FORMATETC { 44 | cfFormat: CF_HDROP.0, 45 | ptd: null_mut(), 46 | dwAspect: DVASPECT_CONTENT.0, 47 | lindex: -1, 48 | tymed: TYMED_HGLOBAL.0 as u32, 49 | }; 50 | 51 | unsafe { 52 | let medium = data_object.GetData(&format)?; 53 | let hdrop = HDROP(medium.u.hGlobal.0); 54 | 55 | let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, None); 56 | if item_count == 0 { 57 | *self.drop_data.borrow_mut() = DropData::None; 58 | return Ok(()); 59 | } 60 | 61 | let mut paths = Vec::with_capacity(item_count as usize); 62 | 63 | for i in 0..item_count { 64 | let characters = DragQueryFileW(hdrop, i, None); 65 | let buffer_size = characters as usize + 1; 66 | let mut buffer = vec![0; buffer_size]; 67 | 68 | DragQueryFileW(hdrop, i, Some(&mut buffer)); 69 | buffer.truncate(buffer_size); 70 | 71 | paths.push(OsString::from_wide(&buffer[..characters as usize]).into()) 72 | } 73 | 74 | *self.drop_data.borrow_mut() = DropData::Files(paths); 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | fn convert_drag_operation(&self, response: EventResponse) -> DROPEFFECT { 81 | if let EventResponse::DropAccepted(operation) = response { 82 | match operation { 83 | DropOperation::None => DROPEFFECT_NONE, 84 | DropOperation::Copy => DROPEFFECT_COPY, 85 | DropOperation::Move => DROPEFFECT_MOVE, 86 | DropOperation::Link => DROPEFFECT_LINK, 87 | } 88 | } else { 89 | DROPEFFECT_NONE 90 | } 91 | } 92 | 93 | fn convert_coordinates(&self, point: &POINTL) -> LogicalPosition { 94 | let Some(window) = self.window.upgrade() else { 95 | return LogicalPosition::default(); 96 | }; 97 | 98 | let scale = window.os_scale(); 99 | 100 | // It looks like MapWindowPoints isn't DPI aware (and neither is ScreenToClient), 101 | // so we need to pre-scale the point here? 102 | // TODO: Find out what's going on 103 | let mut points = [POINT { x: (point.x as f64 / scale) as i32, y: (point.y as f64 / scale) as i32 }]; 104 | 105 | unsafe { MapWindowPoints(Some(HWND_DESKTOP), Some(window.hwnd()), &mut points); } 106 | 107 | PhysicalPosition { 108 | x: points[0].x, 109 | y: points[0].y, 110 | }.to_logical(1.0) 111 | } 112 | } 113 | 114 | #[allow(non_snake_case)] 115 | impl IDropTarget_Impl for DropTarget_Impl { 116 | fn DragEnter(&self, pdataobj: windows_core::Ref<'_, IDataObject>, _grfkeystate: MODIFIERKEYS_FLAGS, pt: &POINTL, pdweffect: *mut DROPEFFECT) -> windows_core::Result<()> { 117 | let Some(window) = self.window.upgrade() else { 118 | return Ok(()); 119 | }; 120 | 121 | self.parse_drag_data(pdataobj)?; 122 | 123 | let response = window.send_event(crate::Event::DragEntered { 124 | position: self.convert_coordinates(pt), 125 | data: self.drop_data.borrow().clone(), 126 | }); 127 | 128 | unsafe { *pdweffect = self.convert_drag_operation(response) }; 129 | 130 | Ok(()) 131 | } 132 | 133 | fn DragOver(&self, _grfkeystate: MODIFIERKEYS_FLAGS, pt: &POINTL, pdweffect: *mut DROPEFFECT) -> windows::core::Result<()> { 134 | let Some(window) = self.window.upgrade() else { 135 | return Ok(()); 136 | }; 137 | 138 | let response = window.send_event(crate::Event::DragMoved { 139 | position: self.convert_coordinates(pt), 140 | data: self.drop_data.borrow().clone(), 141 | }); 142 | 143 | unsafe { *pdweffect = self.convert_drag_operation(response) }; 144 | 145 | Ok(()) 146 | } 147 | 148 | fn DragLeave(&self) -> windows::core::Result<()> { 149 | let Some(window) = self.window.upgrade() else { 150 | return Ok(()); 151 | }; 152 | 153 | window.send_event(crate::Event::DragExited); 154 | 155 | Ok(()) 156 | } 157 | 158 | fn Drop(&self, pdataobj: windows_core::Ref<'_, IDataObject>, _grfkeystate: MODIFIERKEYS_FLAGS, pt: &POINTL, pdweffect: *mut DROPEFFECT) -> windows_core::Result<()> { 159 | let Some(window) = self.window.upgrade() else { 160 | return Ok(()); 161 | }; 162 | 163 | self.parse_drag_data(pdataobj)?; 164 | 165 | let response = window.send_event(crate::Event::DragDropped { 166 | position: self.convert_coordinates(pt), 167 | data: self.drop_data.borrow().clone(), 168 | }); 169 | 170 | unsafe { *pdweffect = self.convert_drag_operation(response) }; 171 | 172 | Ok(()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32/keyboard.rs: -------------------------------------------------------------------------------- 1 | use keyboard_types::Code; 2 | use windows::Win32::UI::Input::KeyboardAndMouse::*; 3 | 4 | pub fn virtual_key_to_keycode(key: VIRTUAL_KEY) -> Code { 5 | match key { 6 | VK_BACK => Code::Backspace, 7 | VK_TAB => Code::Tab, 8 | VK_CLEAR => Code::NumpadClear, 9 | VK_RETURN => Code::Enter, 10 | VK_SHIFT => Code::ShiftLeft, 11 | VK_CONTROL => Code::ControlLeft, 12 | VK_MENU => Code::AltLeft, 13 | VK_PAUSE => Code::Pause, 14 | VK_CAPITAL => Code::CapsLock, 15 | VK_ESCAPE => Code::Escape, 16 | VK_SPACE => Code::Space, 17 | VK_PRIOR => Code::PageUp, 18 | VK_NEXT => Code::PageDown, 19 | VK_END => Code::End, 20 | VK_HOME => Code::Home, 21 | VK_LEFT => Code::ArrowLeft, 22 | VK_UP => Code::ArrowUp, 23 | VK_RIGHT => Code::ArrowRight, 24 | VK_DOWN => Code::ArrowDown, 25 | VK_SELECT => Code::Select, 26 | VK_SNAPSHOT => Code::PrintScreen, 27 | VK_INSERT => Code::Insert, 28 | VK_DELETE => Code::Delete, 29 | VK_HELP => Code::Help, 30 | VK_LWIN => Code::MetaLeft, 31 | VK_RWIN => Code::MetaRight, 32 | VK_APPS => Code::ContextMenu, 33 | VK_SLEEP => Code::Sleep, 34 | VK_NUMPAD0 => Code::Numpad0, 35 | VK_NUMPAD1 => Code::Numpad1, 36 | VK_NUMPAD2 => Code::Numpad2, 37 | VK_NUMPAD3 => Code::Numpad3, 38 | VK_NUMPAD4 => Code::Numpad4, 39 | VK_NUMPAD5 => Code::Numpad5, 40 | VK_NUMPAD6 => Code::Numpad6, 41 | VK_NUMPAD7 => Code::Numpad7, 42 | VK_NUMPAD8 => Code::Numpad8, 43 | VK_NUMPAD9 => Code::Numpad9, 44 | VK_MULTIPLY => Code::NumpadMultiply, 45 | VK_ADD => Code::NumpadAdd, 46 | VK_SUBTRACT => Code::NumpadSubtract, 47 | VK_DECIMAL => Code::NumpadDecimal, 48 | VK_DIVIDE => Code::NumpadDivide, 49 | VK_F1 => Code::F1, 50 | VK_F2 => Code::F2, 51 | VK_F3 => Code::F3, 52 | VK_F4 => Code::F4, 53 | VK_F5 => Code::F5, 54 | VK_F6 => Code::F6, 55 | VK_F7 => Code::F7, 56 | VK_F8 => Code::F8, 57 | VK_F9 => Code::F9, 58 | VK_F10 => Code::F10, 59 | VK_F11 => Code::F11, 60 | VK_F12 => Code::F12, 61 | VK_F13 => Code::F13, 62 | VK_F14 => Code::F14, 63 | VK_F15 => Code::F15, 64 | VK_F16 => Code::F16, 65 | VK_F17 => Code::F17, 66 | VK_F18 => Code::F18, 67 | VK_F19 => Code::F19, 68 | VK_F20 => Code::F20, 69 | VK_F21 => Code::F21, 70 | VK_F22 => Code::F22, 71 | VK_F23 => Code::F23, 72 | VK_F24 => Code::F24, 73 | VK_NUMLOCK => Code::NumLock, 74 | VK_SCROLL => Code::ScrollLock, 75 | VK_OEM_NEC_EQUAL => Code::NumpadEqual, 76 | VK_LSHIFT => Code::ShiftLeft, 77 | VK_RSHIFT => Code::ShiftRight, 78 | VK_LCONTROL => Code::ControlLeft, 79 | VK_RCONTROL => Code::ControlRight, 80 | VK_LMENU => Code::AltLeft, 81 | VK_RMENU => Code::AltRight, 82 | VK_BROWSER_BACK => Code::BrowserBack, 83 | VK_BROWSER_FORWARD => Code::BrowserForward, 84 | VK_BROWSER_REFRESH => Code::BrowserRefresh, 85 | VK_BROWSER_STOP => Code::BrowserStop, 86 | VK_BROWSER_SEARCH => Code::BrowserSearch, 87 | VK_BROWSER_FAVORITES => Code::BrowserFavorites, 88 | VK_BROWSER_HOME => Code::BrowserHome, 89 | VK_VOLUME_MUTE => Code::AudioVolumeMute, 90 | VK_VOLUME_DOWN => Code::AudioVolumeDown, 91 | VK_VOLUME_UP => Code::AudioVolumeUp, 92 | VK_MEDIA_NEXT_TRACK => Code::MediaTrackNext, 93 | VK_MEDIA_PREV_TRACK => Code::MediaTrackPrevious, 94 | VK_MEDIA_STOP => Code::MediaStop, 95 | VK_MEDIA_PLAY_PAUSE => Code::MediaPlayPause, 96 | VK_OEM_1 => Code::Semicolon, 97 | VK_OEM_COMMA => Code::Comma, 98 | VK_OEM_MINUS => Code::Minus, 99 | VK_OEM_PERIOD => Code::Period, 100 | VK_OEM_2 => Code::Slash, 101 | VK_OEM_3 => Code::Backquote, 102 | VK_OEM_4 => Code::BracketLeft, 103 | VK_OEM_5 => Code::Backslash, 104 | VK_OEM_6 => Code::BracketRight, 105 | VK_OEM_7 => Code::Quote, 106 | 107 | _ => Code::Unidentified, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32/message_window.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, ptr::{null, null_mut}, sync::{atomic::{AtomicBool, Ordering}, Arc}}; 2 | 3 | use uuid::Uuid; 4 | use windows::{Win32::{UI::{WindowsAndMessaging::{WNDCLASSW, CS_OWNDC, DefWindowProcW, HICON, HCURSOR, RegisterClassW, CreateWindowExW, WS_EX_NOACTIVATE, GetMessageW, TranslateMessage, DispatchMessageW, WM_CHAR, PostMessageW, SetWindowLongPtrW, GWLP_USERDATA, GetWindowLongPtrW, DestroyWindow, UnregisterClassW, WS_CHILD, WM_KEYDOWN, WM_KEYUP}, Input::KeyboardAndMouse::{SetFocus, VIRTUAL_KEY}}, Graphics::Gdi::HBRUSH, Foundation::{HWND, WPARAM, LPARAM, LRESULT}}, core::PCWSTR}; 5 | use windows_core::BOOL; 6 | 7 | use crate::error::Error; 8 | 9 | use super::{keyboard::virtual_key_to_keycode, to_wstr, PLUGIN_HINSTANCE, WM_USER_CHAR, WM_USER_KEY_DOWN, WM_USER_KEY_UP}; 10 | 11 | pub struct MessageWindow { 12 | hwnd: usize, 13 | main_window_hwnd: usize, 14 | window_class: u16, 15 | } 16 | 17 | impl MessageWindow { 18 | pub fn new(main_window_hwnd: HWND) -> Result { 19 | let class_name = to_wstr("plugin-canvas-message-window-".to_string() + &Uuid::new_v4().simple().to_string()); 20 | let window_name = to_wstr("Message window"); 21 | 22 | let window_class_attributes = WNDCLASSW { 23 | style: CS_OWNDC, 24 | lpfnWndProc: Some(wnd_proc), 25 | cbClsExtra: 0, 26 | cbWndExtra: 0, 27 | hInstance: PLUGIN_HINSTANCE.with(|hinstance| *hinstance), 28 | hIcon: HICON(null_mut()), 29 | hCursor: HCURSOR(null_mut()), 30 | hbrBackground: HBRUSH(null_mut()), 31 | lpszMenuName: PCWSTR(null()), 32 | lpszClassName: PCWSTR(class_name.as_ptr()), 33 | }; 34 | 35 | let window_class = unsafe { RegisterClassW(&window_class_attributes) }; 36 | if window_class == 0 { 37 | return Err(Error::PlatformError("Failed to register window class".into())); 38 | } 39 | 40 | let hwnd = unsafe { CreateWindowExW( 41 | WS_EX_NOACTIVATE, 42 | PCWSTR(window_class as _), 43 | PCWSTR(window_name.as_ptr() as _), 44 | WS_CHILD, 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | Some(main_window_hwnd), 50 | None, 51 | Some(PLUGIN_HINSTANCE.with(|hinstance| *hinstance)), 52 | None, 53 | ).unwrap() }; 54 | 55 | unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, main_window_hwnd.0 as _) }; 56 | 57 | Ok(Self { 58 | hwnd: hwnd.0 as _, 59 | main_window_hwnd: main_window_hwnd.0 as _, 60 | window_class, 61 | }) 62 | } 63 | 64 | pub fn run(&self, running: Arc) { 65 | unsafe { 66 | let hwnd = HWND(self.hwnd as _); 67 | let mut msg = mem::zeroed(); 68 | 69 | while running.load(Ordering::Acquire) { 70 | match GetMessageW(&mut msg, Some(hwnd), 0, 0) { 71 | BOOL(-1) => { 72 | panic!() 73 | } 74 | 75 | BOOL(0) => { 76 | return; 77 | } 78 | 79 | _ => {} 80 | } 81 | 82 | // We can ignore the return value 83 | let _ = TranslateMessage(&msg); 84 | DispatchMessageW(&msg); 85 | } 86 | } 87 | } 88 | 89 | pub fn set_focus(&self, focus: bool) { 90 | let hwnd = HWND(if focus { 91 | self.hwnd 92 | } else { 93 | self.main_window_hwnd 94 | } as _); 95 | 96 | unsafe { SetFocus(Some(hwnd)).unwrap(); } 97 | } 98 | } 99 | 100 | impl Drop for MessageWindow { 101 | fn drop(&mut self) { 102 | unsafe { 103 | // It's ok if this fails; window might already be deleted if our parent window was deleted 104 | DestroyWindow(HWND(self.hwnd as _)).ok(); 105 | UnregisterClassW(PCWSTR(self.window_class as _), Some(PLUGIN_HINSTANCE.with(|hinstance| *hinstance))).unwrap(); 106 | } 107 | } 108 | } 109 | 110 | unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { 111 | let main_window_hwnd = unsafe { HWND(GetWindowLongPtrW(hwnd, GWLP_USERDATA) as _) }; 112 | 113 | match msg { 114 | WM_CHAR => { 115 | unsafe { PostMessageW(Some(main_window_hwnd), WM_USER_CHAR, wparam, lparam).unwrap() }; 116 | LRESULT(0) 117 | }, 118 | 119 | WM_KEYDOWN => { 120 | let keycode = virtual_key_to_keycode(VIRTUAL_KEY(wparam.0 as _)); 121 | unsafe { PostMessageW(Some(main_window_hwnd), WM_USER_KEY_DOWN, WPARAM(keycode as _), LPARAM(0)).unwrap() }; 122 | 123 | LRESULT(0) 124 | } 125 | 126 | WM_KEYUP => { 127 | let keycode = virtual_key_to_keycode(VIRTUAL_KEY(wparam.0 as _)); 128 | unsafe { PostMessageW(Some(main_window_hwnd), WM_USER_KEY_UP, WPARAM(keycode as _), LPARAM(0)).unwrap() }; 129 | 130 | LRESULT(0) 131 | } 132 | 133 | _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/win32/version.rs: -------------------------------------------------------------------------------- 1 | // Adapted from VersionHelpers.h 2 | 3 | use std::mem::size_of; 4 | 5 | use windows::Win32::System::{SystemInformation::{_WIN32_WINNT_WIN10, OSVERSIONINFOEXW, VerifyVersionInfoW, VER_MAJORVERSION, VER_MINORVERSION, VerSetConditionMask}, SystemServices::VER_GREATER_EQUAL}; 6 | 7 | pub fn is_windows10_or_greater() -> bool { 8 | is_windows_version_or_greater(_WIN32_WINNT_WIN10) 9 | } 10 | 11 | fn is_windows_version_or_greater(version: u32) -> bool { 12 | let major_version = (version >> 8) & 0xFF; 13 | let minor_version = version & 0xFF; 14 | 15 | let mut version_info = OSVERSIONINFOEXW { 16 | dwOSVersionInfoSize: size_of::() as u32, 17 | dwMajorVersion: major_version, 18 | dwMinorVersion: minor_version, 19 | 20 | .. Default::default() 21 | }; 22 | 23 | unsafe { 24 | let condition_mask = VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL as u8); 25 | let condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL as u8); 26 | 27 | VerifyVersionInfoW(&mut version_info, VER_MAJORVERSION | VER_MINORVERSION, condition_mask) .is_ok() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/x11.rs: -------------------------------------------------------------------------------- 1 | pub mod cursors; 2 | pub mod keyboard; 3 | pub mod window; 4 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/x11/cursors.rs: -------------------------------------------------------------------------------- 1 | use x11rb::{cursor::Handle, protocol::xproto::Cursor, resource_manager, xcb_ffi::XCBConnection}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Cursors { 5 | pub alias: Cursor, 6 | pub all_scroll: Cursor, 7 | pub arrow: Cursor, 8 | pub cell: Cursor, 9 | pub col_resize: Cursor, 10 | pub context_menu: Cursor, 11 | pub copy: Cursor, 12 | pub crosshair: Cursor, 13 | pub e_resize: Cursor, 14 | pub ew_resize: Cursor, 15 | pub grab: Cursor, 16 | pub grabbing: Cursor, 17 | pub help: Cursor, 18 | pub r#move: Cursor, 19 | pub n_resize: Cursor, 20 | pub ne_resize: Cursor, 21 | pub nesw_resize: Cursor, 22 | pub no_drop: Cursor, 23 | pub not_allowed: Cursor, 24 | pub ns_resize: Cursor, 25 | pub nw_resize: Cursor, 26 | pub nwse_resize: Cursor, 27 | pub pointer: Cursor, 28 | pub progress: Cursor, 29 | pub row_resize: Cursor, 30 | pub s_resize: Cursor, 31 | pub se_resize: Cursor, 32 | pub sw_resize: Cursor, 33 | pub text: Cursor, 34 | pub vertical_text: Cursor, 35 | pub w_resize: Cursor, 36 | pub wait: Cursor, 37 | pub zoom_in: Cursor, 38 | pub zoom_out: Cursor, 39 | } 40 | 41 | impl Cursors { 42 | pub fn new(connection: &XCBConnection, screen: usize) -> Self { 43 | let database = resource_manager::new_from_default(connection).unwrap(); 44 | let handle = Handle::new(connection, screen, &database) 45 | .unwrap() 46 | .reply() 47 | .unwrap(); 48 | 49 | Self { 50 | alias: handle.load_cursor(connection, "alias").unwrap(), 51 | all_scroll: handle.load_cursor(connection, "all-scroll").unwrap(), 52 | arrow: handle.load_cursor(connection, "arrow").unwrap(), 53 | cell: handle.load_cursor(connection, "cell").unwrap(), 54 | col_resize: handle.load_cursor(connection, "col-resize").unwrap(), 55 | context_menu: handle.load_cursor(connection, "context-menu").unwrap(), 56 | copy: handle.load_cursor(connection, "copy").unwrap(), 57 | crosshair: handle.load_cursor(connection, "crosshair").unwrap(), 58 | e_resize: handle.load_cursor(connection, "e-resize").unwrap(), 59 | ew_resize: handle.load_cursor(connection, "ew-resize").unwrap(), 60 | grab: handle.load_cursor(connection, "grab").unwrap(), 61 | grabbing: handle.load_cursor(connection, "grabbing").unwrap(), 62 | help: handle.load_cursor(connection, "help").unwrap(), 63 | r#move: handle.load_cursor(connection, "move").unwrap(), 64 | n_resize: handle.load_cursor(connection, "n-resize").unwrap(), 65 | ne_resize: handle.load_cursor(connection, "ne-resize").unwrap(), 66 | nesw_resize: handle.load_cursor(connection, "nesw-resize").unwrap(), 67 | no_drop: handle.load_cursor(connection, "no-drop").unwrap(), 68 | not_allowed: handle.load_cursor(connection, "not-allowed").unwrap(), 69 | ns_resize: handle.load_cursor(connection, "ns-resize").unwrap(), 70 | nw_resize: handle.load_cursor(connection, "nw-resize").unwrap(), 71 | nwse_resize: handle.load_cursor(connection, "nwse-resize").unwrap(), 72 | pointer: handle.load_cursor(connection, "pointer").unwrap(), 73 | progress: handle.load_cursor(connection, "progress").unwrap(), 74 | row_resize: handle.load_cursor(connection, "row-resize").unwrap(), 75 | s_resize: handle.load_cursor(connection, "s-resize").unwrap(), 76 | se_resize: handle.load_cursor(connection, "se-resize").unwrap(), 77 | sw_resize: handle.load_cursor(connection, "sw-resize").unwrap(), 78 | text: handle.load_cursor(connection, "text").unwrap(), 79 | vertical_text: handle.load_cursor(connection, "vertical-text").unwrap(), 80 | w_resize: handle.load_cursor(connection, "w-resize").unwrap(), 81 | wait: handle.load_cursor(connection, "wait").unwrap(), 82 | zoom_in: handle.load_cursor(connection, "zoom-in").unwrap(), 83 | zoom_out: handle.load_cursor(connection, "zoom-out").unwrap(), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /plugin-canvas/src/platform/x11/keyboard.rs: -------------------------------------------------------------------------------- 1 | use keyboard_types::Code; 2 | 3 | // Based on evded + aliases keycodes 4 | pub fn x11_to_keyboard_types_code(code: u32) -> Code { 5 | match code { 6 | 9 => Code::Escape, 7 | 10 => Code::Digit1, 8 | 11 => Code::Digit2, 9 | 12 => Code::Digit3, 10 | 13 => Code::Digit4, 11 | 14 => Code::Digit5, 12 | 15 => Code::Digit6, 13 | 16 => Code::Digit7, 14 | 17 => Code::Digit8, 15 | 18 => Code::Digit9, 16 | 19 => Code::Digit0, 17 | 20 => Code::Minus, 18 | 21 => Code::Equal, 19 | 22 => Code::Backspace, 20 | 23 => Code::Tab, 21 | 24 => Code::KeyQ, 22 | 25 => Code::KeyW, 23 | 26 => Code::KeyE, 24 | 27 => Code::KeyR, 25 | 28 => Code::KeyT, 26 | 29 => Code::KeyY, 27 | 30 => Code::KeyU, 28 | 31 => Code::KeyI, 29 | 32 => Code::KeyO, 30 | 33 => Code::KeyP, 31 | 34 => Code::BracketLeft, 32 | 35 => Code::BracketRight, 33 | 36 => Code::Enter, 34 | 37 => Code::ControlLeft, 35 | 38 => Code::KeyA, 36 | 39 => Code::KeyS, 37 | 40 => Code::KeyD, 38 | 41 => Code::KeyF, 39 | 42 => Code::KeyG, 40 | 43 => Code::KeyH, 41 | 44 => Code::KeyJ, 42 | 45 => Code::KeyK, 43 | 46 => Code::KeyL, 44 | 47 => Code::Semicolon, 45 | 48 => Code::Quote, 46 | 49 => Code::Backquote, 47 | 50 => Code::ShiftLeft, 48 | 51 => Code::Backslash, 49 | 52 => Code::KeyZ, 50 | 53 => Code::KeyX, 51 | 54 => Code::KeyC, 52 | 55 => Code::KeyV, 53 | 56 => Code::KeyB, 54 | 57 => Code::KeyN, 55 | 58 => Code::KeyM, 56 | 59 => Code::Comma, 57 | 60 => Code::Period, 58 | 61 => Code::Slash, 59 | 62 => Code::ShiftRight, 60 | 63 => Code::NumpadMultiply, 61 | 64 => Code::AltLeft, 62 | 65 => Code::Space, 63 | 66 => Code::CapsLock, 64 | 67 => Code::F1, 65 | 68 => Code::F2, 66 | 69 => Code::F3, 67 | 70 => Code::F4, 68 | 71 => Code::F5, 69 | 72 => Code::F6, 70 | 73 => Code::F7, 71 | 74 => Code::F8, 72 | 75 => Code::F9, 73 | 76 => Code::F10, 74 | 77 => Code::NumLock, 75 | 78 => Code::ScrollLock, 76 | 79 => Code::Numpad7, 77 | 80 => Code::Numpad8, 78 | 81 => Code::Numpad9, 79 | 82 => Code::NumpadSubtract, 80 | 83 => Code::Numpad4, 81 | 84 => Code::Numpad5, 82 | 85 => Code::Numpad6, 83 | 86 => Code::NumpadAdd, 84 | 87 => Code::Numpad1, 85 | 88 => Code::Numpad2, 86 | 89 => Code::Numpad3, 87 | 90 => Code::Numpad0, 88 | 91 => Code::NumpadDecimal, 89 | 94 => Code::IntlBackslash, 90 | 95 => Code::F11, 91 | 96 => Code::F12, 92 | 104 => Code::NumpadEnter, 93 | 105 => Code::ControlRight, 94 | 106 => Code::NumpadDivide, 95 | 107 => Code::PrintScreen, 96 | 108 => Code::AltRight, 97 | 110 => Code::Home, 98 | 111 => Code::ArrowUp, 99 | 112 => Code::PageUp, 100 | 113 => Code::ArrowLeft, 101 | 114 => Code::ArrowRight, 102 | 115 => Code::End, 103 | 116 => Code::ArrowDown, 104 | 117 => Code::PageDown, 105 | 118 => Code::Insert, 106 | 119 => Code::Delete, 107 | 125 => Code::NumpadEqual, 108 | 127 => Code::Pause, 109 | 133 => Code::MetaLeft, 110 | 134 => Code::MetaRight, 111 | 135 => Code::ContextMenu, 112 | 113 | _ => Code::Unidentified 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /plugin-canvas/src/window.rs: -------------------------------------------------------------------------------- 1 | use cursor_icon::CursorIcon; 2 | use raw_window_handle::{RawWindowHandle, HasWindowHandle, HasDisplayHandle}; 3 | 4 | use crate::platform::os_window_handle::OsWindowHandle; 5 | use crate::LogicalPosition; 6 | use crate::dimensions::LogicalSize; 7 | use crate::error::Error; 8 | use crate::event::EventCallback; 9 | use crate::platform::{window::OsWindow, interface::OsWindowInterface}; 10 | 11 | #[derive(Clone)] 12 | pub struct WindowAttributes { 13 | pub(crate) size: LogicalSize, 14 | pub(crate) scale: f64, 15 | } 16 | 17 | impl WindowAttributes { 18 | pub fn new(size: LogicalSize, scale: f64) -> Self { 19 | Self { 20 | size, 21 | scale, 22 | } 23 | } 24 | 25 | pub fn with_size(size: LogicalSize) -> Self { 26 | Self::new(size, 1.0) 27 | } 28 | 29 | pub fn size(&self) -> LogicalSize { 30 | self.size 31 | } 32 | 33 | pub fn scale(&self) -> f64 { 34 | self.scale 35 | } 36 | 37 | pub fn scaled_size(&self) -> LogicalSize { 38 | self.size * self.scale 39 | } 40 | } 41 | 42 | pub struct Window { 43 | attributes: WindowAttributes, 44 | os_window_handle: OsWindowHandle, 45 | } 46 | 47 | impl Window { 48 | pub fn open( 49 | parent: RawWindowHandle, 50 | attributes: WindowAttributes, 51 | event_callback: Box, 52 | ) -> Result { 53 | let os_window_handle = OsWindow::open( 54 | parent, 55 | attributes.clone(), 56 | event_callback, 57 | )?; 58 | 59 | Ok(Self { 60 | attributes, 61 | os_window_handle, 62 | }) 63 | } 64 | 65 | pub fn attributes(&self) -> &WindowAttributes { 66 | &self.attributes 67 | } 68 | 69 | pub fn os_scale(&self) -> f64 { 70 | self.os_window_handle.os_scale() 71 | } 72 | 73 | pub fn resized(&self, size: LogicalSize) { 74 | self.os_window_handle.resized(size); 75 | } 76 | 77 | /// This only needs to be called on Linux 78 | pub fn poll_events(&self) -> Result<(), Error> { 79 | self.os_window_handle.poll_events() 80 | } 81 | 82 | pub fn set_cursor(&self, cursor: Option) { 83 | self.os_window_handle.set_cursor(cursor); 84 | } 85 | 86 | pub fn set_input_focus(&self, focus: bool) { 87 | self.os_window_handle.set_input_focus(focus); 88 | } 89 | 90 | pub fn warp_mouse(&self, position: LogicalPosition) { 91 | self.os_window_handle.warp_mouse(position); 92 | } 93 | } 94 | 95 | impl HasWindowHandle for Window { 96 | fn window_handle(&self) -> Result, raw_window_handle::HandleError> { 97 | self.os_window_handle.window_handle() 98 | } 99 | } 100 | 101 | impl HasDisplayHandle for Window { 102 | fn display_handle(&self) -> Result, raw_window_handle::HandleError> { 103 | self.os_window_handle.display_handle() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug.git" } 8 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> nih_plug_xtask::Result<()> { 2 | nih_plug_xtask::main() 3 | } 4 | --------------------------------------------------------------------------------