├── .cargo └── config ├── .gitattributes ├── xtask ├── src │ └── main.rs └── Cargo.toml ├── bundler.toml ├── .gitignore ├── README.md ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── rust.yml ├── .vscode └── launch.json └── src ├── db_meter.rs ├── biquad_filters.rs ├── ui_knob.rs ├── CustomVerticalSlider.rs └── lib.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --release --" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> nih_plug_xtask::Result<()> { 2 | nih_plug_xtask::main() 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bundler.toml: -------------------------------------------------------------------------------- 1 | # This provides metadata for NIH-plug's `cargo xtask bundle ` plugin 2 | # bundler. This file's syntax is as follows: 3 | # 4 | # [package_name] 5 | # name = "Human Readable Plugin Name" # defaults to 6 | 7 | [Interleaf] 8 | name = "Interleaf" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interleaf 2 | ![Interleaf](https://github.com/ardura/Interleaf/assets/31751444/8a4c466f-5592-4087-ac1f-d32e91916457) 3 | Interleaf was inspired by the Airwindows design of interleaving signals together, and I combined that with my own reimplementation of the RBJ Biquads to create an EQ with up to 10 interleaves per band. 4 | 5 | ## Filter Types 6 | - Low Pass 7 | - High Pass 8 | - Band Pass 9 | - Notch 10 | - Peaking 11 | - Low Shelf 12 | - High Shelf 13 | 14 | ## Other features 15 | 16 | - 2 times oversampling 17 | - Interleaving of 2 through 10 filters, or none at all 18 | - Input/Output gain + Dry/Wet balance 19 | 20 | ## Should I use this over XYZ? 21 | I liked the sound of interleaving and the quirks it can introduce to the signal, hence making this plugin. 22 | It's neither transparent nor too colored in my opinion, but free to enjoy for everyone. 23 | If you like it great! I don't claim it to be anything better than XYZ plugin. 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Interleaf" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Ardura "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/ardura" 8 | description = "An Equalizer" 9 | 10 | [workspace] 11 | members = ["xtask"] 12 | 13 | [lib] 14 | crate-type = ["cdylib","lib"] 15 | 16 | [dependencies] 17 | atomic_float = "0.1" 18 | lazy_static = "1.4.0" 19 | # Remove the `assert_process_allocs` feature to allow allocations on the audio 20 | # thread in debug builds. 21 | 22 | nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "5f4058d1640c68543f64f4f19ed204d4305c2ee8", features = ["assert_process_allocs"] } 23 | nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "5f4058d1640c68543f64f4f19ed204d4305c2ee8"} 24 | once_cell = "1.18.0" 25 | parking_lot = "0.12.1" 26 | 27 | [profile.release] 28 | opt-level = 3 29 | debug = false 30 | lto = "fat" 31 | strip = "symbols" 32 | 33 | [profile.profiling] 34 | inherits = "release" 35 | lto = "off" 36 | opt-level = 0 37 | debug = true 38 | strip = "none" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ardura 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 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: rust 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | branches: '**' 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build_mac_m1: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Run bundler 18 | run: cargo xtask bundle Interleaf --profile release 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: macos_m1_build 22 | path: target/bundled/* 23 | if-no-files-found: warn 24 | build_macos: 25 | runs-on: macos-12 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Run bundler 29 | run: cargo xtask bundle Interleaf --profile release 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: macos_build 33 | path: target/bundled/* 34 | if-no-files-found: warn 35 | build_linux: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: ConorMacBride/install-package@v1.1.0 39 | with: 40 | # Packages to install with apt on Linux 41 | apt: libgl1-mesa-dev libglu1-mesa-dev libxcursor-dev libxkbcommon-x11-dev libatk1.0-dev build-essential libgtk-3-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev 42 | - uses: actions/checkout@v3 43 | - name: Run bundler 44 | run: cargo xtask bundle Interleaf --profile release 45 | - uses: actions/upload-artifact@v4 46 | with: 47 | name: linux_build 48 | path: target/bundled/* 49 | if-no-files-found: warn 50 | build_windows: 51 | runs-on: windows-latest 52 | steps: 53 | - uses: actions/checkout@v4.1.7 54 | - name: Run bundler 55 | run: cargo xtask bundle Interleaf --profile release 56 | - uses: actions/upload-artifact@v4.3.4 57 | with: 58 | name: windows_build 59 | path: target/bundled/* 60 | if-no-files-found: warn 61 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "cppvsdbg", 9 | "request": "attach", 10 | "name": "Debug the plugin inside a DAW", 11 | "processId": "${command:pickProcess}", 12 | "internalConsoleOptions": "openOnSessionStart", 13 | "symbolOptions": { 14 | "searchPaths": ["${workspaceFolder}/target/debug", "${workspaceFolder}/target/profiling"], 15 | "searchMicrosoftSymbolServer": false 16 | } 17 | }, 18 | { 19 | "type": "lldb", 20 | "request": "launch", 21 | "name": "Debug executable 'xtask'", 22 | "cargo": { 23 | "args": [ 24 | "build", 25 | "--bin=xtask", 26 | "--package=xtask" 27 | ], 28 | "filter": { 29 | "name": "xtask", 30 | "kind": "bin" 31 | } 32 | }, 33 | "args": [], 34 | "cwd": "${workspaceFolder}" 35 | }, 36 | { 37 | "type": "lldb", 38 | "request": "launch", 39 | "name": "Debug unit tests in executable 'xtask'", 40 | "cargo": { 41 | "args": [ 42 | "test", 43 | "--no-run", 44 | "--bin=xtask", 45 | "--package=xtask" 46 | ], 47 | "filter": { 48 | "name": "xtask", 49 | "kind": "bin" 50 | } 51 | }, 52 | "args": [], 53 | "cwd": "${workspaceFolder}" 54 | }, 55 | { 56 | "type": "lldb", 57 | "request": "launch", 58 | "name": "Debug unit tests in library 'Subhoofer'", 59 | "cargo": { 60 | "args": [ 61 | "test", 62 | "--no-run", 63 | "--lib", 64 | "--package=Subhoofer" 65 | ], 66 | "filter": { 67 | "name": "Subhoofer", 68 | "kind": "lib" 69 | } 70 | }, 71 | "args": [], 72 | "cwd": "${workspaceFolder}" 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /src/db_meter.rs: -------------------------------------------------------------------------------- 1 | // db_meter.rs - Ardura 2023 2 | // A decibel meter akin to Vizia's nice one in nih-plug 3 | 4 | use nih_plug_egui::egui::{ 5 | lerp, vec2, Color32, NumExt, Pos2, Rect, Response, Sense, Shape, Stroke, TextStyle, Ui, Vec2, 6 | Widget, WidgetText, 7 | }; 8 | 9 | // TODO - let percentage work? 10 | #[allow(dead_code)] 11 | enum DBMeterText { 12 | Custom(WidgetText), 13 | Percentage, 14 | } 15 | 16 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 17 | pub struct DBMeter { 18 | level: f32, 19 | desired_width: Option, 20 | text: Option, 21 | animate: bool, 22 | border_color: Color32, 23 | bar_color: Color32, 24 | background_color: Color32, 25 | } 26 | 27 | #[allow(dead_code)] 28 | impl DBMeter { 29 | /// Progress in the `[0, 1]` range, where `1` means "completed". 30 | pub fn new(level: f32) -> Self { 31 | Self { 32 | level: level.clamp(0.0, 1.0), 33 | desired_width: None, 34 | text: None, 35 | animate: false, 36 | border_color: Color32::BLACK, 37 | bar_color: Color32::GREEN, 38 | background_color: Color32::GRAY, 39 | } 40 | } 41 | 42 | /// The desired width of the bar. Will use all horizontal space if not set. 43 | pub fn desired_width(mut self, desired_width: f32) -> Self { 44 | self.desired_width = Some(desired_width); 45 | self 46 | } 47 | 48 | /// A custom text to display on the progress bar. 49 | pub fn text(mut self, text: impl Into) -> Self { 50 | self.text = Some(DBMeterText::Custom(text.into())); 51 | self 52 | } 53 | 54 | /// Set the color of the outline and text 55 | pub fn set_border_color(&mut self, new_color: Color32) { 56 | self.border_color = new_color; 57 | } 58 | 59 | /// Set the bar color for the meter 60 | pub fn set_bar_color(&mut self, new_color: Color32) { 61 | self.bar_color = new_color; 62 | } 63 | 64 | /// Set the background color 65 | pub fn set_background_color(&mut self, new_color: Color32) { 66 | self.background_color = new_color; 67 | } 68 | } 69 | 70 | impl Widget for DBMeter { 71 | #[allow(unused_variables)] 72 | fn ui(self, ui: &mut Ui) -> Response { 73 | let DBMeter { 74 | level, 75 | desired_width, 76 | text, 77 | animate, 78 | border_color, 79 | bar_color, 80 | background_color, 81 | } = self; 82 | 83 | let animate = animate && level < 1.0; 84 | 85 | let desired_width = 86 | desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0)); 87 | let height = ui.spacing().interact_size.y; 88 | let (outer_rect, response) = 89 | ui.allocate_exact_size(vec2(desired_width, height), Sense::hover()); 90 | 91 | if ui.is_rect_visible(response.rect) { 92 | if animate { 93 | ui.ctx().request_repaint(); 94 | } 95 | 96 | let visuals = ui.style().visuals.clone(); 97 | //let rounding = outer_rect.height() / 2.0; 98 | // Removed rounding 99 | let rounding = 0.0; 100 | ui.painter().rect( 101 | outer_rect, 102 | rounding, 103 | self.background_color, 104 | Stroke::new(1.0, self.border_color), 105 | ); 106 | let inner_rect = Rect::from_min_size( 107 | outer_rect.min, 108 | vec2( 109 | (outer_rect.width() * level).at_least(outer_rect.height()), 110 | outer_rect.height(), 111 | ), 112 | ); 113 | 114 | ui.painter().rect( 115 | inner_rect, 116 | rounding, 117 | if self.level < 1.0 { 118 | self.bar_color 119 | } else { 120 | Color32::RED 121 | }, 122 | Stroke::NONE, 123 | ); 124 | 125 | if animate { 126 | let n_points = 20; 127 | let start_angle = ui.input(|i| i.raw.time).unwrap() * std::f64::consts::TAU; 128 | let end_angle = 129 | start_angle + 240f64.to_radians() * ui.input(|i| i.raw.time).unwrap().sin(); 130 | let circle_radius = rounding - 2.0; 131 | let points: Vec = (0..n_points) 132 | .map(|i| { 133 | let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64); 134 | let (sin, cos) = angle.sin_cos(); 135 | inner_rect.right_center() 136 | + circle_radius * vec2(cos as f32, sin as f32) 137 | + vec2(-rounding, 0.0) 138 | }) 139 | .collect(); 140 | ui.painter() 141 | .add(Shape::line(points, Stroke::new(2.0, self.border_color))); 142 | } 143 | 144 | // Markers 145 | let marker_spacing = outer_rect.width() / 12.0; 146 | let points_x = (outer_rect.left_bottom().x as i32..=outer_rect.right_bottom().x as i32) 147 | .step_by(marker_spacing as usize); 148 | 149 | for x in points_x { 150 | let points: Vec = vec![ 151 | Pos2::new(x as f32, outer_rect.left_bottom().y), 152 | Pos2::new(x as f32, outer_rect.left_bottom().y - 10.0), 153 | ]; 154 | ui.painter() 155 | .add(Shape::line(points, Stroke::new(1.0, self.border_color))); 156 | } 157 | 158 | if let Some(text_kind) = text { 159 | let text = match text_kind { 160 | DBMeterText::Custom(text) => text, 161 | DBMeterText::Percentage => format!("{}%", (level * 100.0) as usize).into(), 162 | }; 163 | let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button); 164 | let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0) 165 | + vec2(ui.spacing().item_spacing.x, 0.0); 166 | let text_color = visuals.override_text_color.unwrap_or(self.border_color); 167 | galley.paint_with_fallback_color( 168 | &ui.painter().with_clip_rect(outer_rect), 169 | text_pos, 170 | text_color, 171 | ); 172 | } 173 | } 174 | 175 | response 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/biquad_filters.rs: -------------------------------------------------------------------------------- 1 | // Biquad filter structures rewritten from RBJ's Audio EQ Cookbook 2 | // I wanted to rewrite it myself to understand it better and make things clearer 3 | // Adapted to rust by Ardura 4 | 5 | use nih_plug::params::enums::Enum; 6 | 7 | // This is for my sanity 8 | const LEFT: usize = 0; 9 | const RIGHT: usize = 1; 10 | 11 | // These are the filter types implemented 12 | #[derive(Clone, Copy, Enum, PartialEq)] 13 | pub(crate) enum FilterType { 14 | Off, 15 | LowPass, 16 | HighPass, 17 | BandPass, 18 | Notch, 19 | Peak, 20 | LowShelf, 21 | HighShelf, 22 | } 23 | 24 | // I wanted these separate from the main struct for readability 25 | #[derive(Clone, Copy)] 26 | struct BiquadCoefficients { 27 | b0: f32, 28 | b1: f32, 29 | b2: f32, 30 | a0: f32, 31 | a1: f32, 32 | a2: f32, 33 | } 34 | 35 | // This assigns our coefficients when passed the intermediate variables 36 | // Nothing to mention here, RBJ has done all the work 37 | impl BiquadCoefficients { 38 | pub fn new(biquad_type: FilterType, alpha: f32, omega: f32, peak_gain: f32) -> Self { 39 | let b0: f32; 40 | let b1: f32; 41 | let b2: f32; 42 | let a0: f32; 43 | let a1: f32; 44 | let a2: f32; 45 | let cos_omega = omega.cos(); 46 | let sin_omega = omega.sin(); 47 | match biquad_type { 48 | FilterType::Off => { 49 | b0 = 0.0; 50 | b1 = 0.0; 51 | b2 = 0.0; 52 | a0 = 0.0; 53 | a1 = 0.0; 54 | a2 = 0.0; 55 | } 56 | FilterType::LowPass => { 57 | b0 = (1.0 - cos_omega)/2.0; 58 | b1 = 1.0 - cos_omega; 59 | b2 = (1.0 - cos_omega)/2.0; 60 | a0 = 1.0 + alpha; 61 | a1 = -2.0 *cos_omega; 62 | a2 = 1.0 - alpha; 63 | }, 64 | FilterType::HighPass => { 65 | b0 = (1.0 + cos_omega)/2.0; 66 | b1 = -(1.0 + cos_omega); 67 | b2 = (1.0 + cos_omega)/2.0; 68 | a0 = 1.0 + alpha; 69 | a1 = -2.0 * cos_omega; 70 | a2 = 1.0 - alpha; 71 | }, 72 | FilterType::BandPass => { 73 | b0 = sin_omega/2.0; 74 | b1 = 0.0; 75 | b2 = -sin_omega/2.0; 76 | a0 = 1.0 + alpha; 77 | a1 = -2.0 * cos_omega; 78 | a2 = 1.0 - alpha; 79 | }, 80 | FilterType::Notch => { 81 | b0 = 1.0; 82 | b1 = -2.0 * cos_omega; 83 | b2 = 1.0; 84 | a0 = 1.0 + alpha; 85 | a1 = -2.0 * cos_omega; 86 | a2 = 1.0 - alpha; 87 | }, 88 | FilterType::Peak => { 89 | let A = (10.0_f32.powf(peak_gain/ 40.0)).sqrt(); 90 | b0 = 1.0 + alpha * A; 91 | b1 = -2.0 * cos_omega; 92 | b2 = 1.0 - alpha * A; 93 | a0 = 1.0 + alpha / A; 94 | a1 = -2.0 * cos_omega; 95 | a2 = 1.0 - alpha / A; 96 | }, 97 | FilterType::LowShelf => { 98 | let A = (10.0_f32.powf(peak_gain/ 40.0)).sqrt(); 99 | let sqrt_a_2_alpha = 2.0 * (A).sqrt() * alpha; 100 | b0 = A * ( ( A + 1.0 ) - ( A - 1.0 ) * cos_omega + sqrt_a_2_alpha ); 101 | b1 = 2.0 * A * ( ( A - 1.0 ) - ( A + 1.0 ) * cos_omega ); 102 | b2 = A * ( ( A + 1.0 ) - ( A - 1.0 ) * cos_omega - sqrt_a_2_alpha ); 103 | a0 = ( A + 1.0 ) + ( A - 1.0 ) * cos_omega + sqrt_a_2_alpha; 104 | a1 = -2.0 * ( ( A - 1.0 ) + ( A + 1.0 ) * cos_omega ); 105 | a2 = ( A + 1.0 ) + ( A - 1.0 ) * cos_omega - sqrt_a_2_alpha; 106 | }, 107 | FilterType::HighShelf => { 108 | let A = (10.0_f32.powf(peak_gain/ 40.0)).sqrt(); 109 | let sqrt_a_2_alpha = 2.0 * (A).sqrt() * alpha; 110 | b0 = A * ( ( A + 1.0 ) + ( A - 1.0 ) * cos_omega + sqrt_a_2_alpha ); 111 | b1 = -2.0 * A * ( ( A - 1.0 ) + ( A + 1.0 ) * cos_omega ); 112 | b2 = A * ( ( A + 1.0 ) + ( A - 1.0 ) * cos_omega - sqrt_a_2_alpha ); 113 | a0 = ( A + 1.0 ) - ( A - 1.0 ) * cos_omega + sqrt_a_2_alpha; 114 | a1 = 2.0 * ( ( A - 1.0 ) - ( A + 1.0 ) * cos_omega ); 115 | a2 = ( A + 1.0 ) - ( A - 1.0 ) * cos_omega - sqrt_a_2_alpha; 116 | }, 117 | } 118 | BiquadCoefficients { 119 | b0: b0, 120 | b1: b1, 121 | b2: b2, 122 | a0: a0, 123 | a1: a1, 124 | a2: a2, 125 | } 126 | } 127 | } 128 | 129 | // This is the main Biquad struct, once more trying to make things clearer 130 | #[derive(Clone, Copy)] 131 | pub(crate) struct Biquad { 132 | // Main controls for the filter 133 | biquad_type: FilterType, 134 | sample_rate: f32, 135 | center_freq: f32, 136 | gain_db: f32, 137 | q_factor: f32, 138 | // Tracks previous outputs 139 | input_history: [[f32; 2]; 2], 140 | output_history: [[f32; 2]; 2], 141 | // Coefficients 142 | coeffs: BiquadCoefficients, 143 | } 144 | 145 | // This is for interleaving biquad structs - Airwindows inspired 146 | // 10 interleave max is just my decision 147 | #[derive(Clone, Copy)] 148 | pub(crate) struct InterleavedBiquad { 149 | interleaves: usize, 150 | current_index: usize, 151 | biquad_array: [Biquad; 10], 152 | } 153 | 154 | impl Biquad { 155 | pub fn new(sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32, biquad_type: FilterType) -> Self { 156 | let omega = 2.0 * std::f32::consts::PI * center_freq / sample_rate; 157 | let alpha = (omega.sin()) / (2.0 * q_factor); 158 | 159 | Biquad { 160 | biquad_type: biquad_type, 161 | sample_rate, 162 | center_freq, 163 | gain_db, 164 | q_factor, 165 | input_history: [[0.0, 0.0]; 2], 166 | output_history: [[0.0, 0.0]; 2], 167 | coeffs: BiquadCoefficients::new(biquad_type, alpha, omega, gain_db), 168 | } 169 | } 170 | 171 | // This is meant to only recalculate when there's an actual update as this method runs often 172 | pub fn update(&mut self, sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32) { 173 | let mut recalc = false; 174 | if self.sample_rate != sample_rate { 175 | self.sample_rate = sample_rate; 176 | recalc = true; 177 | } 178 | if self.center_freq != center_freq { 179 | self.center_freq = center_freq; 180 | recalc = true; 181 | } 182 | if self.gain_db != gain_db { 183 | self.gain_db = gain_db; 184 | recalc = true; 185 | } 186 | if self.q_factor != q_factor { 187 | self.q_factor = q_factor; 188 | recalc = true; 189 | } 190 | if recalc { 191 | // Calculate our intermediate variables from our new info and create new coefficients 192 | let omega = 2.0 * std::f32::consts::PI * center_freq / sample_rate; 193 | let alpha = (omega.sin()) / (2.0 * q_factor); 194 | self.coeffs = BiquadCoefficients::new(self.biquad_type, alpha, omega, self.gain_db); 195 | } 196 | } 197 | 198 | pub fn set_type(&mut self, biquad_type: FilterType) { 199 | if self.biquad_type != biquad_type { 200 | self.biquad_type = biquad_type; 201 | // Calculate our intermediate variables from our new info and create new coefficients 202 | let omega = 2.0 * std::f32::consts::PI * self.center_freq / self.sample_rate; 203 | let alpha = (omega.sin()) / (2.0 * self.q_factor); 204 | self.coeffs = BiquadCoefficients::new(self.biquad_type, alpha, omega, self.gain_db); 205 | } 206 | } 207 | 208 | // I'll handle the oversampling/ordering from the calling thread, I'm trying to K.I.S.S. 209 | pub fn process_sample(&mut self, input_l: f32, input_r: f32) -> (f32, f32) { 210 | if self.biquad_type == FilterType::Off { 211 | return (input_l, input_r) 212 | } 213 | // Using RBJ's Direct Form I straight from the cookbook 214 | let output_l; 215 | let output_r; 216 | // Calculate our current output for the left side 217 | output_l = (self.coeffs.b0 / self.coeffs.a0) * input_l + 218 | (self.coeffs.b1 / self.coeffs.a0) * self.input_history[0][LEFT] + 219 | (self.coeffs.b2 / self.coeffs.a0) * self.input_history[1][LEFT] - 220 | (self.coeffs.a1 / self.coeffs.a0) * self.output_history[0][LEFT] - 221 | (self.coeffs.a2 / self.coeffs.a0) * self.output_history[1][LEFT]; 222 | // Reassign the history variables 223 | self.input_history[1][LEFT] = self.input_history[0][LEFT]; 224 | self.input_history[0][LEFT] = input_l; 225 | self.output_history[1][LEFT] = self.output_history[0][LEFT]; 226 | self.output_history[0][LEFT] = output_l; 227 | 228 | // Calculate our current output for the right side 229 | output_r = (self.coeffs.b0 / self.coeffs.a0) * input_r + 230 | (self.coeffs.b1 / self.coeffs.a0) * self.input_history[0][RIGHT] + 231 | (self.coeffs.b2 / self.coeffs.a0) * self.input_history[1][RIGHT] - 232 | (self.coeffs.a1 / self.coeffs.a0) * self.output_history[0][RIGHT] - 233 | (self.coeffs.a2 / self.coeffs.a0) * self.output_history[1][RIGHT]; 234 | // Reassign the history variables 235 | self.input_history[1][RIGHT] = self.input_history[0][RIGHT]; 236 | self.input_history[0][RIGHT] = input_r; 237 | self.output_history[1][RIGHT] = self.output_history[0][RIGHT]; 238 | self.output_history[0][RIGHT] = output_r; 239 | 240 | (output_l, output_r) 241 | } 242 | } 243 | 244 | impl InterleavedBiquad { 245 | pub fn new(sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32, biquad_type: FilterType, new_interleave: usize) -> Self { 246 | InterleavedBiquad { 247 | interleaves: new_interleave, 248 | current_index: 0, 249 | biquad_array: [Biquad::new(sample_rate, center_freq, gain_db, q_factor, biquad_type); 10], 250 | } 251 | } 252 | 253 | pub fn update(&mut self, sample_rate: f32, center_freq: f32, gain_db: f32, q_factor: f32) { 254 | for biquad in self.biquad_array.iter_mut() { 255 | biquad.update(sample_rate, center_freq, gain_db, q_factor); 256 | } 257 | } 258 | 259 | pub fn set_type(&mut self, biquad_type: FilterType) { 260 | for biquad in self.biquad_array.iter_mut() { 261 | biquad.set_type(biquad_type); 262 | } 263 | } 264 | 265 | pub fn set_interleave(&mut self, new_interleave: usize) { 266 | self.interleaves = new_interleave.clamp(2, 10); 267 | } 268 | 269 | pub fn increment_index(&mut self) { 270 | // Increment our index 271 | self.current_index += 1; 272 | 273 | if self.current_index >= self.interleaves { 274 | self.current_index = 0; 275 | } 276 | } 277 | 278 | pub fn process_sample(&mut self, input_l: f32, input_r: f32) -> (f32, f32) { 279 | let output_l; 280 | let output_r; 281 | (output_l, output_r) = self.biquad_array[self.current_index].process_sample(input_l, input_r); 282 | 283 | // Return 284 | (output_l, output_r) 285 | } 286 | } -------------------------------------------------------------------------------- /src/ui_knob.rs: -------------------------------------------------------------------------------- 1 | // Ardura 2023 - ui_knob.rs - egui + nih-plug parameter widget with customization 2 | // this ui_knob.rs is built off a2aaron's knob base as part of nyasynth and Robbert's ParamSlider code 3 | // https://github.com/a2aaron/nyasynth/blob/canon/src/ui_knob.rs 4 | 5 | use std::{ 6 | f32::consts::TAU, 7 | ops::{Add, Mul, Sub}, 8 | }; 9 | 10 | use lazy_static::lazy_static; 11 | use nih_plug::prelude::{Param, ParamSetter}; 12 | use nih_plug_egui::egui::{ 13 | self, 14 | epaint::{CircleShape, PathShape}, 15 | pos2, Align2, Color32, FontId, Id, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke, Ui, Vec2, 16 | Widget, 17 | }; 18 | use once_cell::sync::Lazy; 19 | 20 | static DRAG_AMOUNT_MEMORY_ID: Lazy = Lazy::new(|| Id::new("drag_amount_memory_id")); 21 | /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the 22 | /// noramlized parameter. 23 | const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015; 24 | 25 | lazy_static! { 26 | static ref DRAG_NORMALIZED_START_VALUE_MEMORY_ID: egui::Id = egui::Id::new((file!(), 0)); 27 | // static ref DRAG_AMOUNT_MEMORY_ID: egui::Id = egui::Id::new((file!(), 1)); 28 | static ref VALUE_ENTRY_MEMORY_ID: egui::Id = egui::Id::new((file!(), 2)); 29 | } 30 | 31 | struct SliderRegion<'a, P: Param> { 32 | param: &'a P, 33 | param_setter: &'a ParamSetter<'a>, 34 | } 35 | 36 | impl<'a, P: Param> SliderRegion<'a, P> { 37 | fn new(param: &'a P, param_setter: &'a ParamSetter) -> Self { 38 | SliderRegion { 39 | param, 40 | param_setter, 41 | } 42 | } 43 | 44 | // Handle the input for a given response. Returns an f32 containing the normalized value of 45 | // the parameter. 46 | fn handle_response(&self, ui: &Ui, response: &Response) -> f32 { 47 | let value = self.param.unmodulated_normalized_value(); 48 | if response.drag_started() { 49 | self.param_setter.begin_set_parameter(self.param); 50 | ui.memory_mut(|i| i.data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, value)) 51 | } 52 | 53 | if response.dragged() { 54 | let delta: f32; 55 | // Invert the y axis, since we want dragging up to increase the value and down to 56 | // decrease it, but drag_delta() has the y-axis increasing downwards. 57 | if ui.input(|i| i.modifiers.shift) { 58 | delta = -response.drag_delta().y * GRANULAR_DRAG_MULTIPLIER; 59 | } else { 60 | delta = -response.drag_delta().y; 61 | } 62 | 63 | ui.memory_mut(|i| { 64 | let value = i.data.get_temp_mut_or(*DRAG_AMOUNT_MEMORY_ID, value); 65 | *value = (*value + delta / 100.0).clamp(0.0, 1.0); 66 | self.param_setter 67 | .set_parameter_normalized(self.param, *value); 68 | }); 69 | } 70 | 71 | // Reset on doubleclick 72 | if response.double_clicked() { 73 | self.param_setter 74 | .set_parameter_normalized(self.param, self.param.default_normalized_value()); 75 | } 76 | 77 | if response.drag_released() { 78 | self.param_setter.end_set_parameter(self.param); 79 | } 80 | value 81 | } 82 | 83 | fn get_string(&self) -> String { 84 | self.param.to_string() 85 | } 86 | } 87 | 88 | pub struct ArcKnob<'a, P: Param> { 89 | slider_region: SliderRegion<'a, P>, 90 | radius: f32, 91 | line_color: Color32, 92 | fill_color: Color32, 93 | center_size: f32, 94 | line_width: f32, 95 | center_to_line_space: f32, 96 | hover_text: bool, 97 | hover_text_content: String, 98 | label_text: String, 99 | show_center_value: bool, 100 | text_size: f32, 101 | outline: bool, 102 | padding: f32, 103 | show_label: bool, 104 | swap_label_and_value: bool, 105 | } 106 | 107 | #[allow(dead_code)] 108 | pub enum KnobStyle { 109 | // Knob_line old presets 110 | SmallTogether, 111 | MediumThin, 112 | LargeMedium, 113 | SmallLarge, 114 | SmallMedium, 115 | SmallSmallOutline, 116 | // Newer presets 117 | NewPresets1, 118 | NewPresets2, 119 | } 120 | 121 | #[allow(dead_code)] 122 | impl<'a, P: Param> ArcKnob<'a, P> { 123 | pub fn for_param(param: &'a P, param_setter: &'a ParamSetter, radius: f32) -> Self { 124 | ArcKnob { 125 | slider_region: SliderRegion::new(param, param_setter), 126 | radius: radius, 127 | line_color: Color32::BLACK, 128 | fill_color: Color32::BLACK, 129 | center_size: 20.0, 130 | line_width: 2.0, 131 | center_to_line_space: 0.0, 132 | hover_text: false, 133 | hover_text_content: String::new(), 134 | text_size: 16.0, 135 | label_text: String::new(), 136 | show_center_value: true, 137 | outline: false, 138 | padding: 10.0, 139 | show_label: true, 140 | swap_label_and_value: true, 141 | } 142 | } 143 | 144 | // Undo newer swap label and value 145 | pub fn set_swap_label_and_value(&mut self, use_old: bool) -> &Self { 146 | self.swap_label_and_value = use_old; 147 | self 148 | } 149 | 150 | // Specify outline drawing 151 | pub fn use_outline(&mut self, new_bool: bool) -> &Self { 152 | self.outline = new_bool; 153 | self 154 | } 155 | 156 | // Specify showing value when mouse-over 157 | pub fn use_hover_text(&mut self, new_bool: bool) -> &Self { 158 | self.hover_text = new_bool; 159 | self 160 | } 161 | 162 | // Specify value when mouse-over 163 | pub fn set_hover_text(&mut self, new_text: String) -> &Self { 164 | self.hover_text_content = new_text; 165 | self 166 | } 167 | 168 | // Specify knob label 169 | pub fn set_label(&mut self, new_label: String) -> &Self { 170 | self.label_text = new_label; 171 | self 172 | } 173 | 174 | // Specify line color for knob outside 175 | pub fn set_line_color(&mut self, new_color: Color32) -> &Self { 176 | self.line_color = new_color; 177 | self 178 | } 179 | 180 | // Specify fill color for knob 181 | pub fn set_fill_color(&mut self, new_color: Color32) -> &Self { 182 | self.fill_color = new_color; 183 | self 184 | } 185 | 186 | // Specify center knob size 187 | pub fn set_center_size(&mut self, size: f32) -> &Self { 188 | self.center_size = size; 189 | self 190 | } 191 | 192 | // Specify line width 193 | pub fn set_line_width(&mut self, width: f32) -> &Self { 194 | self.line_width = width; 195 | self 196 | } 197 | 198 | // Specify distance between center and arc 199 | pub fn set_center_to_line_space(&mut self, new_width: f32) -> &Self { 200 | self.center_to_line_space = new_width; 201 | self 202 | } 203 | 204 | // Set text size for label 205 | pub fn set_text_size(&mut self, text_size: f32) -> &Self { 206 | self.text_size = text_size; 207 | self 208 | } 209 | 210 | // Set knob padding 211 | pub fn set_padding(&mut self, padding: f32) -> &Self { 212 | self.padding = padding; 213 | self 214 | } 215 | 216 | // Set center value of knob visibility 217 | pub fn set_show_center_value(&mut self, new_bool: bool) -> &Self { 218 | self.show_center_value = new_bool; 219 | self 220 | } 221 | 222 | // Set center value of knob visibility 223 | pub fn set_show_label(&mut self, new_bool: bool) -> &Self { 224 | self.show_label = new_bool; 225 | self 226 | } 227 | 228 | pub fn preset_style(&mut self, style_id: KnobStyle) -> &Self { 229 | // These are all calculated off radius to scale better 230 | match style_id { 231 | KnobStyle::SmallTogether => { 232 | self.center_size = self.radius / 4.0; 233 | self.line_width = self.radius / 2.0; 234 | self.center_to_line_space = 0.0; 235 | } 236 | KnobStyle::MediumThin => { 237 | self.center_size = self.radius / 2.0; 238 | self.line_width = self.radius / 8.0; 239 | self.center_to_line_space = self.radius / 4.0; 240 | } 241 | KnobStyle::LargeMedium => { 242 | self.center_size = self.radius / 1.333; 243 | self.line_width = self.radius / 4.0; 244 | self.center_to_line_space = self.radius / 8.0; 245 | } 246 | KnobStyle::SmallLarge => { 247 | self.center_size = self.radius / 8.0; 248 | self.line_width = self.radius / 1.333; 249 | self.center_to_line_space = self.radius / 2.0; 250 | } 251 | KnobStyle::SmallMedium => { 252 | self.center_size = self.radius / 4.0; 253 | self.line_width = self.radius / 2.666; 254 | self.center_to_line_space = self.radius / 1.666; 255 | } 256 | KnobStyle::SmallSmallOutline => { 257 | self.center_size = self.radius / 4.0; 258 | self.line_width = self.radius / 4.0; 259 | self.center_to_line_space = self.radius / 4.0; 260 | self.outline = true; 261 | } 262 | KnobStyle::NewPresets1 => { 263 | self.center_size = self.radius * 0.6; 264 | self.line_width = self.radius * 0.4; 265 | self.center_to_line_space = self.radius * 0.0125; 266 | self.padding = 0.0; 267 | } 268 | KnobStyle::NewPresets2 => { 269 | self.center_size = self.radius * 0.5; 270 | self.line_width = self.radius * 0.5; 271 | self.center_to_line_space = self.radius * 0.0125; 272 | self.swap_label_and_value = true; 273 | self.padding = 0.0; 274 | } 275 | } 276 | self 277 | } 278 | } 279 | 280 | impl<'a, P: Param> Widget for ArcKnob<'a, P> { 281 | fn ui(mut self, ui: &mut Ui) -> Response { 282 | // Figure out the size to reserve on screen for widget 283 | let desired_size = egui::vec2( 284 | self.padding + self.radius * 2.0, 285 | self.padding + self.radius * 2.0, 286 | ); 287 | let response = ui.allocate_response(desired_size, Sense::click_and_drag()); 288 | let value = self.slider_region.handle_response(&ui, &response); 289 | 290 | ui.vertical(|ui| { 291 | let painter = ui.painter_at(response.rect); 292 | let center = response.rect.center(); 293 | 294 | // Draw the arc 295 | let arc_radius = self.center_size + self.center_to_line_space; 296 | let arc_stroke = Stroke::new(self.line_width, self.line_color); 297 | let shape = Shape::Path(PathShape { 298 | points: get_arc_points(center, arc_radius, value, 0.03), 299 | closed: false, 300 | fill: Color32::TRANSPARENT, 301 | stroke: arc_stroke, 302 | }); 303 | painter.add(shape); 304 | 305 | // Draw the outside ring around the control 306 | if self.outline { 307 | let outline_stroke = Stroke::new(1.0, self.fill_color); 308 | let outline_shape = Shape::Path(PathShape { 309 | points: get_arc_points( 310 | center, 311 | self.center_to_line_space + self.line_width, 312 | 1.0, 313 | 0.03, 314 | ), 315 | closed: false, 316 | fill: Color32::TRANSPARENT, 317 | stroke: outline_stroke, 318 | }); 319 | painter.add(outline_shape); 320 | } 321 | 322 | //reset stroke here so we only have fill 323 | let line_stroke = Stroke::new(0.0, Color32::TRANSPARENT); 324 | 325 | // Center of Knob 326 | let circle_shape = Shape::Circle(CircleShape { 327 | center: center, 328 | radius: self.center_size, 329 | stroke: line_stroke, 330 | fill: self.fill_color, 331 | }); 332 | painter.add(circle_shape); 333 | 334 | // Hover text of value 335 | if self.hover_text { 336 | if self.hover_text_content.is_empty() { 337 | self.hover_text_content = self.slider_region.get_string(); 338 | } 339 | ui.allocate_rect( 340 | Rect::from_center_size(center, Vec2::new(self.radius * 2.0, self.radius * 2.0)), 341 | Sense::hover(), 342 | ) 343 | .on_hover_text(self.hover_text_content); 344 | } 345 | 346 | // Label text from response rect bound 347 | let label_y = if self.padding == 0.0 { 348 | 6.0 349 | } else { 350 | self.padding * 2.0 351 | }; 352 | if self.show_label { 353 | let value_pos: Pos2; 354 | let label_pos: Pos2; 355 | if self.swap_label_and_value { 356 | // Newer rearranged positions to put value at bottom of knob 357 | value_pos = Pos2::new( 358 | response.rect.center_bottom().x, 359 | response.rect.center_bottom().y - label_y, 360 | ); 361 | label_pos = Pos2::new(response.rect.center().x, response.rect.center().y); 362 | } else { 363 | // The old value and label positions 364 | label_pos = Pos2::new( 365 | response.rect.center_bottom().x, 366 | response.rect.center_bottom().y - label_y, 367 | ); 368 | value_pos = Pos2::new(response.rect.center().x, response.rect.center().y); 369 | } 370 | 371 | if self.label_text.is_empty() { 372 | painter.text( 373 | value_pos, 374 | Align2::CENTER_CENTER, 375 | self.slider_region.get_string(), 376 | FontId::proportional(self.text_size), 377 | self.line_color, 378 | ); 379 | painter.text( 380 | label_pos, 381 | Align2::CENTER_CENTER, 382 | self.slider_region.param.name(), 383 | FontId::proportional(self.text_size), 384 | self.line_color, 385 | ); 386 | } else { 387 | painter.text( 388 | value_pos, 389 | Align2::CENTER_CENTER, 390 | self.label_text, 391 | FontId::proportional(self.text_size), 392 | self.line_color, 393 | ); 394 | painter.text( 395 | label_pos, 396 | Align2::CENTER_CENTER, 397 | self.slider_region.param.name(), 398 | FontId::proportional(self.text_size), 399 | self.line_color, 400 | ); 401 | } 402 | } 403 | }); 404 | response 405 | } 406 | } 407 | 408 | fn get_arc_points(center: Pos2, radius: f32, value: f32, max_arc_distance: f32) -> Vec { 409 | let start_turns: f32 = 0.625; 410 | let arc_length = lerp(0.0, -0.75, value); 411 | let end_turns = start_turns + arc_length; 412 | 413 | let points = (arc_length.abs() / max_arc_distance).ceil() as usize; 414 | let points = points.max(1); 415 | (0..=points) 416 | .map(|i| { 417 | let t = i as f32 / (points - 1) as f32; 418 | let angle = lerp(start_turns * TAU, end_turns * TAU, t); 419 | let x = radius * angle.cos(); 420 | let y = -radius * angle.sin(); 421 | pos2(x, y) + center.to_vec2() 422 | }) 423 | .collect() 424 | } 425 | 426 | // Moved lerp to this file to reduce dependencies - Ardura 427 | pub fn lerp(start: T, end: T, t: f32) -> T 428 | where 429 | T: Add + Sub + Mul + Copy, 430 | { 431 | (end - start) * t.clamp(0.0, 1.0) + start 432 | } 433 | 434 | pub struct TextSlider<'a, P: Param> { 435 | slider_region: SliderRegion<'a, P>, 436 | location: Rect, 437 | } 438 | 439 | #[allow(dead_code)] 440 | impl<'a, P: Param> TextSlider<'a, P> { 441 | pub fn for_param(param: &'a P, param_setter: &'a ParamSetter, location: Rect) -> Self { 442 | TextSlider { 443 | slider_region: SliderRegion::new(param, param_setter), 444 | location, 445 | } 446 | } 447 | } 448 | 449 | impl<'a, P: Param> Widget for TextSlider<'a, P> { 450 | fn ui(self, ui: &mut Ui) -> Response { 451 | let response = ui.allocate_rect(self.location, Sense::click_and_drag()); 452 | self.slider_region.handle_response(&ui, &response); 453 | 454 | let painter = ui.painter_at(self.location); 455 | let center = self.location.center(); 456 | 457 | // Draw the text 458 | let text = self.slider_region.get_string(); 459 | let anchor = Align2::CENTER_CENTER; 460 | let color = Color32::from(Rgba::WHITE); 461 | let font = FontId::monospace(16.0); 462 | painter.text(center, anchor, text, font, color); 463 | response 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/CustomVerticalSlider.rs: -------------------------------------------------------------------------------- 1 | // Copy of CustomParamSlider from Canopy Reverb modified further into verticality 2 | // Needed to make some weird import changes to get this to work...Definitely should find a better way to do this in future... 3 | // Ardura 4 | use crate::egui::{vec2, Response, Sense, Stroke, TextStyle, Ui, Vec2, Widget, WidgetText}; 5 | use nih_plug::{ 6 | prelude::{Param, ParamSetter}, 7 | wrapper::clap::lazy_static, 8 | }; 9 | use nih_plug_egui::egui::{self}; 10 | use nih_plug_egui::{ 11 | egui::{Color32, Pos2, Rect}, 12 | widgets::util as nUtil, 13 | }; 14 | use parking_lot::Mutex; 15 | use std::sync::Arc; 16 | 17 | /// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the 18 | /// noramlized parameter. 19 | const GRANULAR_DRAG_MULTIPLIER: f32 = 0.0015; 20 | 21 | lazy_static! { 22 | static ref DRAG_NORMALIZED_START_VALUE_MEMORY_ID: egui::Id = egui::Id::new((file!(), 0)); 23 | static ref DRAG_AMOUNT_MEMORY_ID: egui::Id = egui::Id::new((file!(), 1)); 24 | static ref VALUE_ENTRY_MEMORY_ID: egui::Id = egui::Id::new((file!(), 2)); 25 | } 26 | 27 | /// A slider widget similar to [`egui::widgets::Slider`] that knows about NIH-plug parameters ranges 28 | /// and can get values for it. The slider supports double click and control click to reset, 29 | /// shift+drag for granular dragging, text value entry by clicking on the value text. 30 | /// 31 | /// TODO: Vertical orientation 32 | /// TODO: Check below for more input methods that should be added 33 | /// TODO: Decouple the logic from the drawing so we can also do things like nobs without having to 34 | /// repeat everything 35 | /// TODO: Add WidgetInfo annotations for accessibility 36 | #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] 37 | pub struct ParamSlider<'a, P: Param> { 38 | param: &'a P, 39 | setter: &'a ParamSetter<'a>, 40 | 41 | draw_value: bool, 42 | slider_width: Option, 43 | slider_height: Option, 44 | // Added in reversed function to have bar drawn other way 45 | reversed: bool, 46 | background_set_color: Color32, 47 | bar_set_color: Color32, 48 | use_padding: bool, 49 | 50 | /// Will be set in the `ui()` function so we can request keyboard input focus on Alt+click. 51 | keyboard_focus_id: Option, 52 | } 53 | 54 | #[allow(dead_code)] 55 | impl<'a, P: Param> ParamSlider<'a, P> { 56 | /// Create a new slider for a parameter. Use the other methods to modify the slider before 57 | /// passing it to [`Ui::add()`]. 58 | pub fn for_param(param: &'a P, setter: &'a ParamSetter<'a>) -> Self { 59 | Self { 60 | param, 61 | setter, 62 | 63 | draw_value: true, 64 | slider_width: None, 65 | slider_height: None, 66 | // Added in reversed function to have bar drawn other way 67 | reversed: false, 68 | background_set_color: Color32::TEMPORARY_COLOR, 69 | bar_set_color: Color32::TEMPORARY_COLOR, 70 | use_padding: false, 71 | 72 | // I removed this because it was causing errors on plugin load somehow in FL 73 | keyboard_focus_id: None, 74 | } 75 | } 76 | 77 | pub fn override_colors( 78 | mut self, 79 | background_set_color: Color32, 80 | bar_set_color: Color32, 81 | ) -> Self { 82 | self.background_set_color = background_set_color; 83 | self.bar_set_color = bar_set_color; 84 | self 85 | } 86 | 87 | /// Don't draw the text slider's current value after the slider. 88 | pub fn without_value(mut self) -> Self { 89 | self.draw_value = false; 90 | self 91 | } 92 | 93 | /// Set a custom width for the slider. 94 | pub fn with_width(mut self, width: f32) -> Self { 95 | self.slider_width = Some(width); 96 | self 97 | } 98 | 99 | pub fn with_height(mut self, height: f32) -> Self { 100 | self.slider_height = Some(height); 101 | self 102 | } 103 | 104 | /// Set reversed bar drawing - Ardura 105 | pub fn set_reversed(mut self, reversed: bool) -> Self { 106 | self.reversed = reversed; 107 | self 108 | } 109 | 110 | pub fn use_padding(mut self, use_padding: bool) -> Self { 111 | self.use_padding = use_padding; 112 | self 113 | } 114 | 115 | fn plain_value(&self) -> P::Plain { 116 | self.param.modulated_plain_value() 117 | } 118 | 119 | fn normalized_value(&self) -> f32 { 120 | self.param.modulated_normalized_value() 121 | } 122 | 123 | fn string_value(&self) -> String { 124 | self.param.to_string() 125 | } 126 | 127 | /// Enable the keyboard entry part of the widget. 128 | fn begin_keyboard_entry(&self, ui: &Ui) { 129 | ui.memory_mut(|i| i.request_focus(self.keyboard_focus_id.unwrap())); 130 | 131 | // Always initialize the field to the current value, that seems nicer than having to 132 | // being typing from scratch 133 | let value_entry_mutex = ui.memory_mut(|i| { 134 | i.data 135 | .get_temp_mut_or_default::>>(*VALUE_ENTRY_MEMORY_ID) 136 | .clone() 137 | }); 138 | *value_entry_mutex.lock() = self.string_value(); 139 | } 140 | 141 | fn keyboard_entry_active(&self, ui: &Ui) -> bool { 142 | ui.memory(|i| i.has_focus(self.keyboard_focus_id.unwrap())) 143 | } 144 | 145 | fn begin_drag(&self) { 146 | self.setter.begin_set_parameter(self.param); 147 | } 148 | 149 | fn set_normalized_value(&self, normalized: f32) { 150 | // This snaps to the nearest plain value if the parameter is stepped in some way. 151 | // TODO: As an optimization, we could add a `const CONTINUOUS: bool` to the parameter to 152 | // avoid this normalized->plain->normalized conversion for parameters that don't need 153 | // it 154 | let value = self.param.preview_plain(normalized); 155 | if value != self.plain_value() { 156 | self.setter.set_parameter(self.param, value); 157 | } 158 | } 159 | 160 | /// Begin and end drag still need to be called when using this. Returns `false` if the string 161 | /// could no tbe parsed. 162 | fn set_from_string(&self, string: &str) -> bool { 163 | match self.param.string_to_normalized_value(string) { 164 | Some(normalized_value) => { 165 | self.set_normalized_value(normalized_value); 166 | true 167 | } 168 | None => false, 169 | } 170 | } 171 | 172 | /// Begin and end drag still need to be called when using this.. 173 | fn reset_param(&self) { 174 | self.setter 175 | .set_parameter(self.param, self.param.default_plain_value()); 176 | } 177 | 178 | fn granular_drag(&self, ui: &Ui, drag_delta: Vec2) { 179 | // Remember the intial position when we started with the granular drag. This value gets 180 | // reset whenever we have a normal interaction with the slider. 181 | let start_value = if Self::get_drag_amount_memory(ui) == 0.0 { 182 | Self::set_drag_normalized_start_value_memory(ui, self.normalized_value()); 183 | self.normalized_value() 184 | } else { 185 | Self::get_drag_normalized_start_value_memory(ui) 186 | }; 187 | 188 | let total_drag_distance = drag_delta.x + Self::get_drag_amount_memory(ui); 189 | Self::set_drag_amount_memory(ui, total_drag_distance); 190 | 191 | self.set_normalized_value( 192 | (start_value + (total_drag_distance * GRANULAR_DRAG_MULTIPLIER)).clamp(0.0, 1.0), 193 | ); 194 | } 195 | 196 | fn end_drag(&self) { 197 | self.setter.end_set_parameter(self.param); 198 | } 199 | 200 | fn get_drag_normalized_start_value_memory(ui: &Ui) -> f32 { 201 | ui.memory(|i| { 202 | i.data 203 | .get_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID) 204 | .unwrap_or(0.5) 205 | }) 206 | } 207 | 208 | fn set_drag_normalized_start_value_memory(ui: &Ui, amount: f32) { 209 | ui.memory_mut(|i| { 210 | i.data 211 | .insert_temp(*DRAG_NORMALIZED_START_VALUE_MEMORY_ID, amount) 212 | }); 213 | } 214 | 215 | fn get_drag_amount_memory(ui: &Ui) -> f32 { 216 | ui.memory(|i| i.data.get_temp(*DRAG_AMOUNT_MEMORY_ID).unwrap_or(0.0)) 217 | } 218 | 219 | fn set_drag_amount_memory(ui: &Ui, amount: f32) { 220 | ui.memory_mut(|i| i.data.insert_temp(*DRAG_AMOUNT_MEMORY_ID, amount)); 221 | } 222 | 223 | fn slider_ui(&self, ui: &mut Ui, response: &mut Response) { 224 | // Handle user input 225 | // TODO: Optionally (since it can be annoying) add scrolling behind a builder option 226 | if response.drag_started() { 227 | // When beginning a drag or dragging normally, reset the memory used to keep track of 228 | // our granular drag 229 | self.begin_drag(); 230 | Self::set_drag_amount_memory(ui, 0.0); 231 | } 232 | if let Some(click_pos) = response.interact_pointer_pos() { 233 | if ui.input(|i| i.modifiers.command) { 234 | // Like double clicking, Ctrl+Click should reset the parameter 235 | self.reset_param(); 236 | response.mark_changed(); 237 | } else if ui.input(|i| i.modifiers.shift) { 238 | // And shift dragging should switch to a more granular input method 239 | self.granular_drag(ui, response.drag_delta()); 240 | response.mark_changed(); 241 | } else { 242 | // This was changed to y values from X to read the up down 243 | let proportion = 244 | egui::emath::remap_clamp(click_pos.y, response.rect.y_range(), 0.0..=1.0) 245 | as f64; 246 | self.set_normalized_value(1.0 - proportion as f32); 247 | response.mark_changed(); 248 | Self::set_drag_amount_memory(ui, 0.0); 249 | } 250 | } 251 | if response.double_clicked() { 252 | self.reset_param(); 253 | response.mark_changed(); 254 | } 255 | if response.drag_released() { 256 | self.end_drag(); 257 | } 258 | 259 | // And finally draw the thing 260 | if ui.is_rect_visible(response.rect) { 261 | // Also flipped these orders for vertical 262 | if self.reversed { 263 | // We'll do a flat widget with background -> filled foreground -> slight border 264 | ui.painter() 265 | .rect_filled(response.rect, 0.0, ui.visuals().widgets.inactive.bg_fill); 266 | } else { 267 | ui.painter() 268 | .rect_filled(response.rect, 0.0, ui.visuals().selection.bg_fill); 269 | } 270 | 271 | let filled_proportion = self.normalized_value(); 272 | if filled_proportion > 0.0 { 273 | let left_bottom = response.rect.left_bottom(); 274 | let right_bottom = response.rect.right_bottom(); 275 | let rect_points = [ 276 | Pos2::new( 277 | left_bottom.x, 278 | left_bottom.y - (response.rect.height() * filled_proportion), 279 | ), // Top left 280 | Pos2::new( 281 | right_bottom.x, 282 | right_bottom.y - (response.rect.height() * filled_proportion), 283 | ), // Top right 284 | left_bottom, 285 | right_bottom, 286 | ]; 287 | 288 | //let mut filled_rect = response.rect; 289 | let mut filled_rect = Rect::from_points(&rect_points); 290 | filled_rect.set_bottom(response.rect.bottom()); 291 | // This was changed from width to make it height 292 | filled_rect.set_height(response.rect.height() * filled_proportion); 293 | 294 | // Added to reverse filling - Ardura 295 | // Vertical has this flipped to make sense vs the horizontal bar 296 | if self.reversed { 297 | let filled_bg = if response.dragged() { 298 | if self.bar_set_color == Color32::TEMPORARY_COLOR { 299 | nUtil::add_hsv(ui.visuals().selection.bg_fill, 0.0, -0.1, 0.1) 300 | } else { 301 | nUtil::add_hsv(self.bar_set_color, 0.0, -0.1, 0.1) 302 | } 303 | } else { 304 | if self.bar_set_color == Color32::TEMPORARY_COLOR { 305 | ui.visuals().selection.bg_fill 306 | } else { 307 | self.bar_set_color 308 | } 309 | }; 310 | ui.painter().rect_filled(filled_rect, 0.0, filled_bg); 311 | } else { 312 | let filled_bg = if response.dragged() { 313 | nUtil::add_hsv(ui.visuals().widgets.inactive.bg_fill, 0.0, -0.1, 0.1) 314 | } else { 315 | ui.visuals().widgets.inactive.bg_fill 316 | }; 317 | ui.painter().rect_filled(filled_rect, 0.0, filled_bg); 318 | } 319 | } 320 | 321 | if self.background_set_color == Color32::TEMPORARY_COLOR { 322 | ui.painter().rect_stroke( 323 | response.rect, 324 | 0.0, 325 | Stroke::new(1.0, ui.visuals().widgets.active.bg_fill), 326 | ); 327 | } else { 328 | ui.painter().rect_stroke( 329 | response.rect, 330 | 0.0, 331 | Stroke::new(1.0, self.background_set_color), 332 | ); 333 | } 334 | } 335 | } 336 | 337 | fn value_ui(&self, ui: &mut Ui) { 338 | let visuals = ui.visuals().widgets.inactive; 339 | let should_draw_frame = ui.visuals().button_frame; 340 | let padding = if self.use_padding { 341 | ui.spacing().button_padding 342 | } else { 343 | ui.spacing().button_padding / 2.0 344 | }; 345 | 346 | /* 347 | // I had to comment this out since the init of ParamSlider breaks because of the keyboard focus not existing in FL 348 | // I'm not sure how the original ParamSlider code works as a result :| 349 | 350 | // Either show the parameter's label, or show a text entry field if the parameter's label 351 | // has been clicked on 352 | let keyboard_focus_id = self.keyboard_focus_id.unwrap(); 353 | if self.keyboard_entry_active(ui) { 354 | let value_entry_mutex = ui 355 | .memory() 356 | .data 357 | .get_temp_mut_or_default::>>(*VALUE_ENTRY_MEMORY_ID) 358 | .clone(); 359 | let mut value_entry = value_entry_mutex.lock(); 360 | 361 | ui.add( 362 | TextEdit::singleline(&mut *value_entry) 363 | .id(keyboard_focus_id) 364 | .font(TextStyle::Monospace), 365 | ); 366 | if ui.input().key_pressed(Key::Escape) { 367 | // Cancel when pressing escape 368 | ui.memory().surrender_focus(keyboard_focus_id); 369 | } else if ui.input().key_pressed(Key::Enter) { 370 | // And try to set the value by string when pressing enter 371 | self.begin_drag(); 372 | self.set_from_string(&value_entry); 373 | self.end_drag(); 374 | 375 | ui.memory().surrender_focus(keyboard_focus_id); 376 | } 377 | } else { 378 | */ 379 | let text = WidgetText::from(self.string_value()).into_galley( 380 | ui, 381 | None, 382 | ui.available_width() - (padding.x * 2.0), 383 | TextStyle::Button, 384 | ); 385 | 386 | let response = ui.allocate_response(text.size() + (padding * 2.0), Sense::click()); 387 | if response.clicked() { 388 | //self.begin_keyboard_entry(ui); 389 | } 390 | 391 | if ui.is_rect_visible(response.rect) { 392 | if should_draw_frame { 393 | let fill = visuals.bg_fill; 394 | let stroke = visuals.bg_stroke; 395 | ui.painter().rect( 396 | response.rect.expand(visuals.expansion), 397 | visuals.rounding, 398 | fill, 399 | stroke, 400 | ); 401 | } 402 | 403 | let text_pos = ui 404 | .layout() 405 | .align_size_within_rect(text.size(), response.rect.shrink2(padding)) 406 | .min; 407 | text.paint_with_visuals(ui.painter(), text_pos, &visuals); 408 | } 409 | //} 410 | } 411 | } 412 | 413 | impl Widget for ParamSlider<'_, P> { 414 | fn ui(self, ui: &mut Ui) -> Response { 415 | let slider_width = self 416 | .slider_width 417 | .unwrap_or_else(|| ui.spacing().interact_size.y); 418 | let slider_height = self 419 | .slider_height 420 | .unwrap_or_else(|| ui.spacing().interact_size.x); 421 | 422 | // Changed to vertical to fix the label 423 | ui.vertical(|ui| { 424 | // Allocate space 425 | let mut response = ui 426 | .vertical(|ui| { 427 | //ui.allocate_space(vec2(slider_width, slider_height)); 428 | let response = ui.allocate_response( 429 | vec2(slider_width, slider_height), 430 | Sense::click_and_drag(), 431 | ); 432 | response 433 | }) 434 | .inner; 435 | 436 | self.slider_ui(ui, &mut response); 437 | if self.draw_value { 438 | self.value_ui(ui); 439 | } 440 | 441 | response 442 | }) 443 | .inner 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | mod CustomVerticalSlider; 4 | mod biquad_filters; 5 | mod db_meter; 6 | mod ui_knob; 7 | use atomic_float::AtomicF32; 8 | use nih_plug::prelude::*; 9 | use nih_plug_egui::{ 10 | create_egui_editor, 11 | egui::{self, Color32, FontId, Rect, RichText, Rounding, Ui}, 12 | EguiState, 13 | }; 14 | use std::{ 15 | ops::RangeInclusive, 16 | sync::{Arc, Mutex}, 17 | }; 18 | use CustomVerticalSlider::ParamSlider as VerticalParamSlider; 19 | use biquad_filters::FilterType; 20 | 21 | /************************************************** 22 | * Interleaf by Ardura 23 | * This is a parametric EQ using interleaved biquads 24 | * of up to 10 interleaves with 5 bands! 25 | * 26 | * Build with: cargo xtask bundle Interleaf --profile release 27 | * ************************************************/ 28 | 29 | // GUI Colors 30 | const LIGHT: Color32 = Color32::from_rgb(206,185,146); 31 | const MAIN: Color32 = Color32::from_rgb(115,147,126); 32 | const BLACK: Color32 = Color32::from_rgb(4, 7, 14); 33 | const ACCENT: Color32 = Color32::from_rgb(48,99,142); 34 | 35 | // Plugin sizing 36 | const WIDTH: u32 = 370; 37 | const HEIGHT: u32 = 660; 38 | 39 | // Constants 40 | const VERT_BAR_HEIGHT: f32 = 260.0; 41 | const VERT_BAR_WIDTH: f32 = 32.0; 42 | 43 | /// The time it takes for the peak meter to decay by 12 dB after switching to complete silence. 44 | const PEAK_METER_DECAY_MS: f64 = 360.0; 45 | 46 | const MAIN_FONT: nih_plug_egui::egui::FontId = FontId::monospace(8.0); 47 | 48 | #[derive(Clone, Copy)] 49 | struct EQ { 50 | non_interleave_bands: [biquad_filters::Biquad; 5], 51 | interleave_bands: [biquad_filters::InterleavedBiquad; 5], 52 | } 53 | 54 | pub struct Interleaf { 55 | params: Arc, 56 | 57 | // normalize the peak meter's response based on the sample rate with this 58 | out_meter_decay_weight: f32, 59 | 60 | // Equalizer made of peaks 61 | equalizer: Arc>, 62 | 63 | // The current data for the different meters 64 | out_meter: Arc, 65 | in_meter: Arc, 66 | } 67 | 68 | #[derive(Params)] 69 | struct InterleafParams { 70 | #[persist = "editor-state"] 71 | editor_state: Arc, 72 | 73 | #[id = "input_gain"] 74 | pub input_gain: FloatParam, 75 | 76 | #[id = "output_gain"] 77 | pub output_gain: FloatParam, 78 | 79 | #[id = "dry_wet"] 80 | pub dry_wet: FloatParam, 81 | 82 | #[id = "oversampling"] 83 | pub oversampling: FloatParam, 84 | 85 | #[id = "interleaves"] 86 | pub interleaves: FloatParam, 87 | 88 | // Bands 89 | #[id = "freq_band_0"] 90 | pub freq_band_0: FloatParam, 91 | 92 | #[id = "freq_band_1"] 93 | pub freq_band_1: FloatParam, 94 | 95 | #[id = "freq_band_2"] 96 | pub freq_band_2: FloatParam, 97 | 98 | #[id = "freq_band_3"] 99 | pub freq_band_3: FloatParam, 100 | 101 | #[id = "freq_band_4"] 102 | pub freq_band_4: FloatParam, 103 | 104 | // Gain 105 | #[id = "gain_band_0"] 106 | pub gain_band_0: FloatParam, 107 | 108 | #[id = "gain_band_1"] 109 | pub gain_band_1: FloatParam, 110 | 111 | #[id = "gain_band_2"] 112 | pub gain_band_2: FloatParam, 113 | 114 | #[id = "gain_band_3"] 115 | pub gain_band_3: FloatParam, 116 | 117 | #[id = "gain_band_4"] 118 | pub gain_band_4: FloatParam, 119 | 120 | // Resonance 121 | #[id = "res_band_0"] 122 | pub res_band_0: FloatParam, 123 | 124 | #[id = "res_band_1"] 125 | pub res_band_1: FloatParam, 126 | 127 | #[id = "res_band_2"] 128 | pub res_band_2: FloatParam, 129 | 130 | #[id = "res_band_3"] 131 | pub res_band_3: FloatParam, 132 | 133 | #[id = "res_band_4"] 134 | pub res_band_4: FloatParam, 135 | 136 | // Band Types 137 | #[id = "type_0"] 138 | pub type_0: EnumParam, 139 | 140 | #[id = "type_1"] 141 | pub type_1: EnumParam, 142 | 143 | #[id = "type_2"] 144 | pub type_2: EnumParam, 145 | 146 | #[id = "type_3"] 147 | pub type_3: EnumParam, 148 | 149 | #[id = "type_4"] 150 | pub type_4: EnumParam, 151 | } 152 | 153 | impl Default for Interleaf { 154 | fn default() -> Self { 155 | Self { 156 | params: Arc::new(InterleafParams::default()), 157 | out_meter_decay_weight: 1.0, 158 | out_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)), 159 | in_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)), 160 | // Hard code to 44100, will update in processing 161 | equalizer: Arc::new(Mutex::new(EQ { 162 | non_interleave_bands: [ 163 | // These defaults don't matter as they are overwritten immediately 164 | biquad_filters::Biquad::new( 44100.0,800.0,0.0, 0.707, FilterType::Peak) 165 | // 5 Bands of the above 166 | ; 5 167 | ], 168 | interleave_bands: [ 169 | // These defaults don't matter as they are overwritten immediately 170 | biquad_filters::InterleavedBiquad::new( 44100.0,800.0,0.0, 0.707, FilterType::Peak, 2) 171 | // 5 Bands of the above 172 | ; 5 173 | ], 174 | })), 175 | } 176 | } 177 | } 178 | 179 | impl Default for InterleafParams { 180 | fn default() -> Self { 181 | Self { 182 | editor_state: EguiState::from_size(WIDTH, HEIGHT), 183 | 184 | // Input gain dB parameter 185 | input_gain: FloatParam::new( 186 | "In", 187 | util::db_to_gain(0.0), 188 | FloatRange::Skewed { 189 | min: util::db_to_gain(-12.0), 190 | max: util::db_to_gain(12.0), 191 | factor: FloatRange::gain_skew_factor(-12.0, 12.0), 192 | }, 193 | ) 194 | .with_smoother(SmoothingStyle::Logarithmic(50.0)) 195 | .with_value_to_string(formatters::v2s_f32_rounded(1)) 196 | .with_string_to_value(formatters::s2v_f32_gain_to_db()), 197 | 198 | // Output gain parameter 199 | output_gain: FloatParam::new( 200 | "Out", 201 | util::db_to_gain(0.0), 202 | FloatRange::Skewed { 203 | min: util::db_to_gain(-12.0), 204 | max: util::db_to_gain(12.0), 205 | factor: FloatRange::gain_skew_factor(-12.0, 12.0), 206 | }, 207 | ) 208 | .with_smoother(SmoothingStyle::Logarithmic(50.0)) 209 | .with_value_to_string(formatters::v2s_f32_rounded(1)) 210 | .with_string_to_value(formatters::s2v_f32_gain_to_db()), 211 | 212 | // Dry/Wet parameter 213 | dry_wet: FloatParam::new("Wet", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }) 214 | .with_unit("%") 215 | .with_value_to_string(formatters::v2s_f32_percentage(2)) 216 | .with_string_to_value(formatters::s2v_f32_percentage()), 217 | 218 | oversampling: FloatParam::new( 219 | "x2", 220 | 0.0, 221 | FloatRange::Linear { 222 | min: 0.0, 223 | max: 1.0, 224 | }, 225 | ) 226 | .with_value_to_string(format_x2()) 227 | .with_step_size(1.0), 228 | 229 | interleaves: FloatParam::new( 230 | "Interleave", 231 | 4.0, 232 | FloatRange::Linear { 233 | min: 1.0, 234 | max: 10.0, 235 | }, 236 | ) 237 | .with_step_size(1.0) 238 | .with_value_to_string(format_interleave()), 239 | 240 | // Non Param Buttons 241 | freq_band_0: FloatParam::new( 242 | "Band 0", 243 | 200.0, 244 | FloatRange::Skewed { 245 | min: 1.0, 246 | max: 20000.0, 247 | factor: 0.3, 248 | }, 249 | ) 250 | .with_step_size(1.0) 251 | .with_smoother(SmoothingStyle::Linear(5.0)) 252 | .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, false)), 253 | freq_band_1: FloatParam::new( 254 | "Band 1", 255 | 800.0, 256 | FloatRange::Skewed { 257 | min: 1.0, 258 | max: 20000.0, 259 | factor: 0.4, 260 | }, 261 | ) 262 | .with_step_size(1.0) 263 | .with_smoother(SmoothingStyle::Linear(5.0)) 264 | .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, false)), 265 | freq_band_2: FloatParam::new( 266 | "Band 2", 267 | 2000.0, 268 | FloatRange::Skewed { 269 | min: 1.0, 270 | max: 20000.0, 271 | factor: 0.5, 272 | }, 273 | ) 274 | .with_step_size(1.0) 275 | .with_smoother(SmoothingStyle::Linear(5.0)) 276 | .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, false)), 277 | freq_band_3: FloatParam::new( 278 | "Band 3", 279 | 8000.0, 280 | FloatRange::Skewed { 281 | min: 1.0, 282 | max: 20000.0, 283 | factor: 0.7, 284 | }, 285 | ) 286 | .with_step_size(1.0) 287 | .with_smoother(SmoothingStyle::Linear(5.0)) 288 | .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, false)), 289 | freq_band_4: FloatParam::new( 290 | "Band 4", 291 | 15000.0, 292 | FloatRange::Skewed { 293 | min: 1.0, 294 | max: 20000.0, 295 | factor: 1.0, 296 | }, 297 | ) 298 | .with_step_size(1.0) 299 | .with_smoother(SmoothingStyle::Linear(5.0)) 300 | .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, false)), 301 | 302 | // Gain Bands 303 | gain_band_0: FloatParam::new( 304 | "Gain 0", 305 | 0.0, 306 | FloatRange::Linear { 307 | min: -12.0, 308 | max: 12.0, 309 | }, 310 | ) 311 | .with_smoother(SmoothingStyle::Linear(50.0)) 312 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 313 | gain_band_1: FloatParam::new( 314 | "Gain 1", 315 | 0.0, 316 | FloatRange::Linear { 317 | min: -12.0, 318 | max: 12.0, 319 | }, 320 | ) 321 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 322 | gain_band_2: FloatParam::new( 323 | "Gain 2", 324 | 0.0, 325 | FloatRange::Linear { 326 | min: -12.0, 327 | max: 12.0, 328 | }, 329 | ) 330 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 331 | gain_band_3: FloatParam::new( 332 | "Gain 3", 333 | 0.0, 334 | FloatRange::Linear { 335 | min: -12.0, 336 | max: 12.0, 337 | }, 338 | ) 339 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 340 | gain_band_4: FloatParam::new( 341 | "Gain 4", 342 | 0.0, 343 | FloatRange::Linear { 344 | min: -12.0, 345 | max: 12.0, 346 | }, 347 | ) 348 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 349 | 350 | // Res Bands 351 | res_band_0: FloatParam::new( 352 | "Res 0", 353 | 0.707, 354 | FloatRange::Linear { 355 | min: 0.01, 356 | max: 1.0, 357 | }, 358 | ) 359 | .with_smoother(SmoothingStyle::Linear(50.0)) 360 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 361 | res_band_1: FloatParam::new( 362 | "Res 1", 363 | 0.707, 364 | FloatRange::Linear { 365 | min: 0.01, 366 | max: 1.0, 367 | }, 368 | ) 369 | .with_smoother(SmoothingStyle::Linear(50.0)) 370 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 371 | res_band_2: FloatParam::new( 372 | "Res 2", 373 | 0.707, 374 | FloatRange::Linear { 375 | min: 0.01, 376 | max: 1.0, 377 | }, 378 | ) 379 | .with_smoother(SmoothingStyle::Linear(50.0)) 380 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 381 | res_band_3: FloatParam::new( 382 | "Res 3", 383 | 0.707, 384 | FloatRange::Linear { 385 | min: 0.01, 386 | max: 1.0, 387 | }, 388 | ) 389 | .with_smoother(SmoothingStyle::Linear(50.0)) 390 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 391 | res_band_4: FloatParam::new( 392 | "Res 4", 393 | 0.707, 394 | FloatRange::Linear { 395 | min: 0.01, 396 | max: 1.0, 397 | }, 398 | ) 399 | .with_smoother(SmoothingStyle::Linear(50.0)) 400 | .with_value_to_string(formatters::v2s_f32_rounded(1)), 401 | 402 | // Band types 403 | type_0: EnumParam::new("Type 0", FilterType::LowShelf), 404 | type_1: EnumParam::new("Type 1", FilterType::Peak), 405 | type_2: EnumParam::new("Type 2", FilterType::Peak), 406 | type_3: EnumParam::new("Type 3", FilterType::Peak), 407 | type_4: EnumParam::new("Type 4", FilterType::HighShelf), 408 | } 409 | } 410 | } 411 | 412 | impl Interleaf { 413 | fn create_band_gui( 414 | ui: &mut Ui, 415 | type_param: &EnumParam, 416 | freq_param: &FloatParam, 417 | gain_param: &FloatParam, 418 | res_param: &FloatParam, 419 | setter: &ParamSetter<'_>, 420 | knob_size: f32, 421 | ) { 422 | ui.vertical(|ui| { 423 | ui.add( 424 | VerticalParamSlider::for_param(gain_param, setter) 425 | .with_width(VERT_BAR_WIDTH * 2.0) 426 | .with_height(VERT_BAR_HEIGHT) 427 | .set_reversed(true), 428 | ); 429 | let mut type_knob = ui_knob::ArcKnob::for_param(type_param, setter, knob_size); 430 | type_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 431 | type_knob.set_fill_color(ACCENT); 432 | type_knob.set_line_color(MAIN); 433 | type_knob.set_show_label(true); 434 | type_knob.set_text_size(10.0); 435 | ui.add(type_knob); 436 | 437 | let mut freq_knob = ui_knob::ArcKnob::for_param(freq_param, setter, knob_size); 438 | freq_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 439 | freq_knob.set_fill_color(ACCENT); 440 | freq_knob.set_line_color(MAIN); 441 | freq_knob.set_show_label(true); 442 | freq_knob.set_text_size(10.0); 443 | ui.add(freq_knob); 444 | 445 | let mut res_knob = ui_knob::ArcKnob::for_param(res_param, setter, knob_size); 446 | res_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 447 | res_knob.set_fill_color(ACCENT); 448 | res_knob.set_line_color(MAIN); 449 | res_knob.set_show_label(true); 450 | res_knob.set_text_size(10.0); 451 | ui.add(res_knob); 452 | }); 453 | } 454 | } 455 | 456 | impl Plugin for Interleaf { 457 | const NAME: &'static str = "Interleaf"; 458 | const VENDOR: &'static str = "Ardura"; 459 | const URL: &'static str = "https://github.com/ardura"; 460 | const EMAIL: &'static str = "azviscarra@gmail.com"; 461 | 462 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 463 | 464 | // This looks like it's flexible for running the plugin in mono or stereo 465 | const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ 466 | AudioIOLayout { 467 | main_input_channels: NonZeroU32::new(2), 468 | main_output_channels: NonZeroU32::new(2), 469 | ..AudioIOLayout::const_default() 470 | }, 471 | AudioIOLayout { 472 | main_input_channels: NonZeroU32::new(1), 473 | main_output_channels: NonZeroU32::new(1), 474 | ..AudioIOLayout::const_default() 475 | }, 476 | ]; 477 | 478 | const SAMPLE_ACCURATE_AUTOMATION: bool = true; 479 | 480 | type SysExMessage = (); 481 | type BackgroundTask = (); 482 | 483 | fn params(&self) -> Arc { 484 | self.params.clone() 485 | } 486 | 487 | fn editor(&mut self, _async_executor: AsyncExecutor) -> Option> { 488 | let params = self.params.clone(); 489 | let in_meter = self.in_meter.clone(); 490 | let out_meter = self.out_meter.clone(); 491 | create_egui_editor( 492 | self.params.editor_state.clone(), 493 | (), 494 | |_, _| {}, 495 | move |egui_ctx, setter, _state| { 496 | egui::CentralPanel::default().show(egui_ctx, |ui| { 497 | // Assign default colors 498 | ui.style_mut().visuals.widgets.inactive.bg_stroke.color = BLACK; 499 | ui.style_mut().visuals.widgets.inactive.bg_fill = BLACK; 500 | ui.style_mut().visuals.widgets.active.fg_stroke.color = ACCENT; 501 | ui.style_mut().visuals.widgets.active.bg_stroke.color = ACCENT; 502 | ui.style_mut().visuals.widgets.open.fg_stroke.color = ACCENT; 503 | ui.style_mut().visuals.widgets.open.bg_fill = MAIN; 504 | // Lettering on param sliders 505 | ui.style_mut().visuals.widgets.inactive.fg_stroke.color = ACCENT; 506 | // Background of the bar in param sliders 507 | ui.style_mut().visuals.selection.bg_fill = ACCENT; 508 | ui.style_mut().visuals.selection.stroke.color = ACCENT; 509 | // Unfilled background of the bar 510 | ui.style_mut().visuals.widgets.noninteractive.bg_fill = MAIN; 511 | 512 | // Set default font 513 | ui.style_mut().override_font_id = Some(MAIN_FONT); 514 | 515 | // Trying to draw background colors as rects 516 | ui.painter().rect_filled( 517 | Rect::from_x_y_ranges( 518 | RangeInclusive::new(0.0, WIDTH as f32), 519 | RangeInclusive::new(0.0, HEIGHT as f32), 520 | ), 521 | Rounding::none(), 522 | BLACK, 523 | ); 524 | 525 | // GUI Structure 526 | ui.vertical(|ui| { 527 | // Spacing :) 528 | ui.label( 529 | RichText::new(" Interleaf - Interleaving EQ") 530 | .font(FontId::proportional(14.0)) 531 | .color(LIGHT), 532 | ) 533 | .on_hover_text("by Ardura!"); 534 | 535 | // Peak Meters 536 | let in_meter = 537 | util::gain_to_db(in_meter.load(std::sync::atomic::Ordering::Relaxed)); 538 | let in_meter_text = if in_meter > util::MINUS_INFINITY_DB { 539 | format!("{in_meter:.1} dBFS Input") 540 | } else { 541 | String::from("-inf dBFS Input") 542 | }; 543 | let in_meter_normalized = (in_meter + 60.0) / 60.0; 544 | ui.allocate_space(egui::Vec2::splat(2.0)); 545 | let mut in_meter_obj = 546 | db_meter::DBMeter::new(in_meter_normalized).text(in_meter_text); 547 | in_meter_obj.set_background_color(BLACK); 548 | in_meter_obj.set_bar_color(LIGHT); 549 | in_meter_obj.set_border_color(MAIN); 550 | ui.add(in_meter_obj); 551 | 552 | let out_meter = 553 | util::gain_to_db(out_meter.load(std::sync::atomic::Ordering::Relaxed)); 554 | let out_meter_text = if out_meter > util::MINUS_INFINITY_DB { 555 | format!("{out_meter:.1} dBFS Output") 556 | } else { 557 | String::from("-inf dBFS Output") 558 | }; 559 | let out_meter_normalized = (out_meter + 60.0) / 60.0; 560 | ui.allocate_space(egui::Vec2::splat(2.0)); 561 | let mut out_meter_obj = 562 | db_meter::DBMeter::new(out_meter_normalized).text(out_meter_text); 563 | out_meter_obj.set_background_color(BLACK); 564 | out_meter_obj.set_bar_color(ACCENT); 565 | out_meter_obj.set_border_color(MAIN); 566 | ui.add(out_meter_obj); 567 | 568 | ui.separator(); 569 | 570 | // UI Control area 571 | egui::scroll_area::ScrollArea::horizontal() 572 | .auto_shrink([true; 2]) 573 | .show(ui, |ui| { 574 | ui.vertical(|ui|{ 575 | ui.horizontal(|ui| { 576 | // Draw our band UI 577 | Self::create_band_gui( 578 | ui, 579 | ¶ms.type_0, 580 | ¶ms.freq_band_0, 581 | ¶ms.gain_band_0, 582 | ¶ms.res_band_0, 583 | setter, 584 | VERT_BAR_WIDTH, 585 | ); 586 | Self::create_band_gui( 587 | ui, 588 | ¶ms.type_1, 589 | ¶ms.freq_band_1, 590 | ¶ms.gain_band_1, 591 | ¶ms.res_band_1, 592 | setter, 593 | VERT_BAR_WIDTH, 594 | ); 595 | Self::create_band_gui( 596 | ui, 597 | ¶ms.type_2, 598 | ¶ms.freq_band_2, 599 | ¶ms.gain_band_2, 600 | ¶ms.res_band_2, 601 | setter, 602 | VERT_BAR_WIDTH, 603 | ); 604 | Self::create_band_gui( 605 | ui, 606 | ¶ms.type_3, 607 | ¶ms.freq_band_3, 608 | ¶ms.gain_band_3, 609 | ¶ms.res_band_3, 610 | setter, 611 | VERT_BAR_WIDTH, 612 | ); 613 | Self::create_band_gui( 614 | ui, 615 | ¶ms.type_4, 616 | ¶ms.freq_band_4, 617 | ¶ms.gain_band_4, 618 | ¶ms.res_band_4, 619 | setter, 620 | VERT_BAR_WIDTH, 621 | ); 622 | }); 623 | // Bottom controls 624 | ui.horizontal(|ui| { 625 | let mut os_knob = ui_knob::ArcKnob::for_param( 626 | ¶ms.oversampling, 627 | setter, 628 | VERT_BAR_WIDTH - 4.0, 629 | ); 630 | os_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 631 | os_knob.set_text_size(12.0); 632 | os_knob.set_fill_color(ACCENT); 633 | os_knob.set_line_color(LIGHT); 634 | ui.add(os_knob); 635 | 636 | let mut interleave_knob = ui_knob::ArcKnob::for_param( 637 | ¶ms.interleaves, 638 | setter, 639 | VERT_BAR_WIDTH - 4.0, 640 | ); 641 | interleave_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 642 | interleave_knob.set_text_size(8.0); 643 | interleave_knob.set_fill_color(ACCENT); 644 | interleave_knob.set_line_color(LIGHT); 645 | ui.add(interleave_knob); 646 | 647 | let mut gain_knob = ui_knob::ArcKnob::for_param( 648 | ¶ms.input_gain, 649 | setter, 650 | VERT_BAR_WIDTH - 4.0, 651 | ); 652 | gain_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 653 | gain_knob.set_text_size(10.0); 654 | gain_knob.set_fill_color(ACCENT); 655 | gain_knob.set_line_color(LIGHT); 656 | ui.add(gain_knob); 657 | 658 | let mut output_knob = ui_knob::ArcKnob::for_param( 659 | ¶ms.output_gain, 660 | setter, 661 | VERT_BAR_WIDTH - 4.0, 662 | ); 663 | output_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 664 | output_knob.set_text_size(10.0); 665 | output_knob.set_fill_color(ACCENT); 666 | output_knob.set_line_color(LIGHT); 667 | ui.add(output_knob); 668 | 669 | let mut dry_wet_knob = ui_knob::ArcKnob::for_param( 670 | ¶ms.dry_wet, 671 | setter, 672 | VERT_BAR_WIDTH - 4.0, 673 | ); 674 | dry_wet_knob.preset_style(ui_knob::KnobStyle::NewPresets2); 675 | dry_wet_knob.set_text_size(10.0); 676 | dry_wet_knob.set_fill_color(ACCENT); 677 | dry_wet_knob.set_line_color(LIGHT); 678 | ui.add(dry_wet_knob); 679 | }); 680 | }); 681 | }); 682 | }); 683 | }); 684 | }, 685 | ) 686 | } 687 | 688 | fn initialize( 689 | &mut self, 690 | _audio_io_layout: &AudioIOLayout, 691 | buffer_config: &BufferConfig, 692 | _context: &mut impl InitContext, 693 | ) -> bool { 694 | // After `PEAK_METER_DECAY_MS` milliseconds of pure silence, the peak meter's value should 695 | // have dropped by 12 dB 696 | self.out_meter_decay_weight = 0.25f64 697 | .powf((buffer_config.sample_rate as f64 * PEAK_METER_DECAY_MS / 1000.0).recip()) 698 | as f32; 699 | 700 | true 701 | } 702 | 703 | fn process( 704 | &mut self, 705 | buffer: &mut Buffer, 706 | _aux: &mut AuxiliaryBuffers, 707 | _context: &mut impl ProcessContext, 708 | ) -> ProcessStatus { 709 | let arc_eq = self.equalizer.clone(); 710 | for mut channel_samples in buffer.iter_samples() { 711 | let mut out_amplitude = 0.0; 712 | let mut in_amplitude = 0.0; 713 | let mut processed_sample_l: f32 = 0.0; 714 | let mut processed_sample_r: f32 = 0.0; 715 | let num_samples = channel_samples.len(); 716 | 717 | let gain = util::gain_to_db(self.params.input_gain.smoothed.next()); 718 | let output_gain = self.params.output_gain.smoothed.next(); 719 | let dry_wet = self.params.dry_wet.value(); 720 | 721 | // Split left and right same way original subhoofer did 722 | let mut in_l: f32 = *channel_samples.get_mut(0).unwrap(); 723 | let mut in_r: f32 = *channel_samples.get_mut(1).unwrap(); 724 | 725 | // Make sure we are always on the correct sample rate, then update our EQ 726 | let mut eq = arc_eq.lock().unwrap(); 727 | 728 | let sr = _context.transport().sample_rate; 729 | 730 | // Apply our input gain to our incoming signal 731 | in_l *= util::db_to_gain(gain); 732 | in_r *= util::db_to_gain(gain); 733 | 734 | // Calculate our amplitude for the decibel meter 735 | in_amplitude += in_l + in_r; 736 | 737 | // Set our interleaves 738 | let interleave = self.params.interleaves.value(); 739 | for filter in eq.interleave_bands.iter_mut() { 740 | filter.set_interleave(interleave as usize); 741 | } 742 | 743 | // Update our types 744 | eq.interleave_bands[0].set_type(self.params.type_0.value()); 745 | eq.interleave_bands[1].set_type(self.params.type_1.value()); 746 | eq.interleave_bands[2].set_type(self.params.type_2.value()); 747 | eq.interleave_bands[3].set_type(self.params.type_3.value()); 748 | eq.interleave_bands[4].set_type(self.params.type_4.value()); 749 | eq.non_interleave_bands[0].set_type(self.params.type_0.value()); 750 | eq.non_interleave_bands[1].set_type(self.params.type_1.value()); 751 | eq.non_interleave_bands[2].set_type(self.params.type_2.value()); 752 | eq.non_interleave_bands[3].set_type(self.params.type_3.value()); 753 | eq.non_interleave_bands[4].set_type(self.params.type_4.value()); 754 | 755 | if interleave >= 2.0 { 756 | // Use the interleaved biquads 757 | eq.interleave_bands[0].update( 758 | sr, 759 | self.params.freq_band_0.value(), 760 | self.params.gain_band_0.value(), 761 | self.params.res_band_0.value(), 762 | ); 763 | eq.interleave_bands[1].update( 764 | sr, 765 | self.params.freq_band_1.value(), 766 | self.params.gain_band_1.value(), 767 | self.params.res_band_1.value(), 768 | ); 769 | eq.interleave_bands[2].update( 770 | sr, 771 | self.params.freq_band_2.value(), 772 | self.params.gain_band_2.value(), 773 | self.params.res_band_2.value(), 774 | ); 775 | eq.interleave_bands[3].update( 776 | sr, 777 | self.params.freq_band_3.value(), 778 | self.params.gain_band_3.value(), 779 | self.params.res_band_3.value(), 780 | ); 781 | eq.interleave_bands[4].update( 782 | sr, 783 | self.params.freq_band_4.value(), 784 | self.params.gain_band_4.value(), 785 | self.params.res_band_4.value(), 786 | ); 787 | 788 | // Perform processing on the sample using the filters 789 | let mut temp_l: f32 = -2.0; 790 | let mut temp_r: f32 = -2.0; 791 | for filter in eq.interleave_bands.iter_mut() { 792 | for i in 0..=self.params.oversampling.value() as usize { 793 | match i { 794 | 0 => { 795 | if temp_l == -2.0 { 796 | // This is the first time we run a filter at all 797 | (temp_l, temp_r) = filter.process_sample(in_l, in_r); 798 | } else { 799 | // This is not the first time or first filter but first iteration of "A filter" 800 | (temp_l, temp_r) = filter.process_sample(temp_l, temp_r); 801 | } 802 | }, 803 | _ => { 804 | // These are subsequent filter iterations for any filter in the order 805 | (temp_l, temp_r) = filter.process_sample(temp_l, temp_r); 806 | } 807 | } 808 | filter.increment_index(); 809 | } 810 | 811 | // Sum up our output 812 | processed_sample_l = temp_l; 813 | processed_sample_r = temp_r; 814 | } 815 | } else { 816 | // No interleaved biquads 817 | eq.non_interleave_bands[0].update( 818 | sr, 819 | self.params.freq_band_0.value(), 820 | self.params.gain_band_0.value(), 821 | self.params.res_band_0.value(), 822 | ); 823 | eq.non_interleave_bands[1].update( 824 | sr, 825 | self.params.freq_band_1.value(), 826 | self.params.gain_band_1.value(), 827 | self.params.res_band_1.value(), 828 | ); 829 | eq.non_interleave_bands[2].update( 830 | sr, 831 | self.params.freq_band_2.value(), 832 | self.params.gain_band_2.value(), 833 | self.params.res_band_2.value(), 834 | ); 835 | eq.non_interleave_bands[3].update( 836 | sr, 837 | self.params.freq_band_3.value(), 838 | self.params.gain_band_3.value(), 839 | self.params.res_band_3.value(), 840 | ); 841 | eq.non_interleave_bands[4].update( 842 | sr, 843 | self.params.freq_band_4.value(), 844 | self.params.gain_band_4.value(), 845 | self.params.res_band_4.value(), 846 | ); 847 | 848 | // Perform processing on the sample using the filters 849 | let mut temp_l: f32 = -2.0; 850 | let mut temp_r: f32 = -2.0; 851 | for filter in eq.non_interleave_bands.iter_mut() { 852 | for i in 0..=self.params.oversampling.value() as usize { 853 | match i { 854 | 0 => { 855 | if temp_l == -2.0 { 856 | // This is the first time we run a filter at all 857 | (temp_l, temp_r) = filter.process_sample(in_l, in_r); 858 | } else { 859 | // This is not the first time or first filter but first iteration of "A filter" 860 | (temp_l, temp_r) = filter.process_sample(temp_l, temp_r); 861 | } 862 | }, 863 | _ => { 864 | // These are subsequent filter iterations for any filter in the order 865 | (temp_l, temp_r) = filter.process_sample(temp_l, temp_r); 866 | } 867 | } 868 | 869 | } 870 | // Sum up our output 871 | processed_sample_l = temp_l; 872 | processed_sample_r = temp_r; 873 | } 874 | } 875 | 876 | // Calculate dry/wet mix 877 | let wet_gain = dry_wet; 878 | let dry_gain = 1.0 - dry_wet; 879 | processed_sample_l = in_l * dry_gain + processed_sample_l * wet_gain; 880 | processed_sample_r = in_r * dry_gain + processed_sample_r * wet_gain; 881 | 882 | // Output gain 883 | processed_sample_l *= output_gain; 884 | processed_sample_r *= output_gain; 885 | 886 | // Assign back so we can output our processed sounds 887 | *channel_samples.get_mut(0).unwrap() = processed_sample_l; 888 | *channel_samples.get_mut(1).unwrap() = processed_sample_r; 889 | 890 | out_amplitude += processed_sample_l + processed_sample_r; 891 | 892 | // To save resources, a plugin can (and probably should!) only perform expensive 893 | // calculations that are only displayed on the GUI while the GUI is open 894 | if self.params.editor_state.is_open() { 895 | // Input gain meter 896 | in_amplitude = (in_amplitude / num_samples as f32).abs(); 897 | let current_in_meter = self.in_meter.load(std::sync::atomic::Ordering::Relaxed); 898 | let new_in_meter = if in_amplitude > current_in_meter { 899 | in_amplitude 900 | } else { 901 | current_in_meter * self.out_meter_decay_weight 902 | + in_amplitude * (1.0 - self.out_meter_decay_weight) 903 | }; 904 | self.in_meter 905 | .store(new_in_meter, std::sync::atomic::Ordering::Relaxed); 906 | 907 | // Output gain meter 908 | out_amplitude = (out_amplitude / num_samples as f32).abs(); 909 | let current_out_meter = self.out_meter.load(std::sync::atomic::Ordering::Relaxed); 910 | let new_out_meter = if out_amplitude > current_out_meter { 911 | out_amplitude 912 | } else { 913 | current_out_meter * self.out_meter_decay_weight 914 | + out_amplitude * (1.0 - self.out_meter_decay_weight) 915 | }; 916 | self.out_meter 917 | .store(new_out_meter, std::sync::atomic::Ordering::Relaxed); 918 | } 919 | } 920 | ProcessStatus::Normal 921 | } 922 | 923 | const MIDI_INPUT: MidiConfig = MidiConfig::None; 924 | 925 | const MIDI_OUTPUT: MidiConfig = MidiConfig::None; 926 | 927 | const HARD_REALTIME_ONLY: bool = false; 928 | 929 | fn task_executor(&mut self) -> TaskExecutor { 930 | // In the default implementation we can simply ignore the value 931 | Box::new(|_| ()) 932 | } 933 | 934 | fn filter_state(_state: &mut PluginState) {} 935 | 936 | fn reset(&mut self) {} 937 | 938 | fn deactivate(&mut self) {} 939 | } 940 | 941 | impl ClapPlugin for Interleaf { 942 | const CLAP_ID: &'static str = "com.ardura.Interleaf"; 943 | const CLAP_DESCRIPTION: Option<&'static str> = Some("An EQ"); 944 | const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); 945 | const CLAP_SUPPORT_URL: Option<&'static str> = None; 946 | const CLAP_FEATURES: &'static [ClapFeature] = &[ 947 | ClapFeature::AudioEffect, 948 | ClapFeature::Stereo, 949 | ClapFeature::Mono, 950 | ClapFeature::Utility, 951 | ClapFeature::Equalizer, 952 | ]; 953 | } 954 | 955 | impl Vst3Plugin for Interleaf { 956 | const VST3_CLASS_ID: [u8; 16] = *b"InterleafAAAAAAA"; 957 | const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = 958 | &[Vst3SubCategory::Fx, Vst3SubCategory::Eq]; 959 | } 960 | 961 | nih_export_clap!(Interleaf); 962 | nih_export_vst3!(Interleaf); 963 | 964 | // I use this when I want to remove label and unit from a param in gui 965 | pub fn format_nothing() -> Arc String + Send + Sync> { 966 | Arc::new(move |_| String::new()) 967 | } 968 | 969 | // This formats the interleave knob 970 | pub fn format_interleave() -> Arc String + Send + Sync> { 971 | Arc::new(move | input_number | if input_number < 2.0 {String::from("Off")} else {String::from(input_number.to_string())}) 972 | } 973 | 974 | // This formats the x2 knob - this is like this because of using the value to control looping 975 | pub fn format_x2() -> Arc String + Send + Sync> { 976 | Arc::new(move | input_number | if input_number == 1.0 {String::from("On")} else {String::from("Off")}) 977 | } --------------------------------------------------------------------------------