├── .cargo └── config.toml ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── docs.yml │ ├── readme-Linux.txt │ ├── readme-Windows.txt │ ├── readme-macOS.txt │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bundler.toml ├── cargo_nih_plug ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── nih_plug_derive ├── Cargo.toml ├── src │ ├── enums.rs │ ├── lib.rs │ └── params.rs └── tests │ ├── params.rs │ └── persist.rs ├── nih_plug_egui ├── Cargo.toml ├── README.md └── src │ ├── editor.rs │ ├── lib.rs │ ├── resizable_window.rs │ ├── widgets.rs │ └── widgets │ ├── generic_ui.rs │ ├── param_slider.rs │ └── util.rs ├── nih_plug_iced ├── Cargo.toml ├── README.md └── src │ ├── assets.rs │ ├── editor.rs │ ├── lib.rs │ ├── widgets.rs │ ├── widgets │ ├── generic_ui.rs │ ├── param_slider.rs │ ├── peak_meter.rs │ └── util.rs │ └── wrapper.rs ├── nih_plug_vizia ├── Cargo.toml ├── README.md ├── assets │ ├── theme.css │ └── widgets.css └── src │ ├── assets.rs │ ├── editor.rs │ ├── lib.rs │ ├── vizia_assets.rs │ ├── widgets.rs │ └── widgets │ ├── generic_ui.rs │ ├── param_base.rs │ ├── param_button.rs │ ├── param_slider.rs │ ├── peak_meter.rs │ ├── resize_handle.rs │ └── util.rs ├── nih_plug_xtask ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── symbols.rs │ └── util.rs ├── plugins ├── buffr_glitch │ ├── CHANGELOG.md │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── buffer.rs │ │ ├── envelope.rs │ │ └── lib.rs ├── crisp │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── editor.rs │ │ ├── filter.rs │ │ ├── lib.rs │ │ └── pcg.rs ├── crossover │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── crossover.rs │ │ ├── crossover │ │ ├── fir.rs │ │ ├── fir │ │ │ └── filter.rs │ │ ├── iir.rs │ │ └── iir │ │ │ └── biquad.rs │ │ └── lib.rs ├── diopser │ ├── CHANGELOG.md │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── editor.rs │ │ ├── editor │ │ ├── analyzer.rs │ │ ├── button.rs │ │ ├── safe_mode.rs │ │ ├── slider.rs │ │ ├── theme.css │ │ └── xy_pad.rs │ │ ├── filter.rs │ │ ├── lib.rs │ │ ├── params.rs │ │ └── spectrum.rs ├── examples │ ├── byo_gui_gl │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── byo_gui_softbuffer │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── byo_gui_wgpu │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── gain │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── gain_gui_egui │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── gain_gui_iced │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── editor.rs │ │ │ └── lib.rs │ ├── gain_gui_vizia │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── editor.rs │ │ │ ├── lib.rs │ │ │ └── main.rs │ ├── midi_inverter │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── poly_mod_synth │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── sine │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── stft │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── sysex │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── loudness_war_winner │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── filter.rs │ │ └── lib.rs ├── puberty_simulator │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── safety_limiter │ ├── CHANGELOG.md │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── soft_vacuum │ ├── CHANGELOG.md │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── hard_vacuum.rs │ │ ├── lib.rs │ │ └── oversampling.rs └── spectral_compressor │ ├── CHANGELOG.md │ ├── COPYING │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── analyzer.rs │ ├── compressor_bank.rs │ ├── curve.rs │ ├── dry_wet_mixer.rs │ ├── editor.rs │ ├── editor │ ├── analyzer.rs │ ├── mode_button.rs │ └── theme.css │ └── lib.rs ├── src ├── audio_setup.rs ├── buffer.rs ├── buffer │ ├── blocks.rs │ └── samples.rs ├── context.rs ├── context │ ├── gui.rs │ ├── init.rs │ ├── process.rs │ └── remote_controls.rs ├── debug.rs ├── editor.rs ├── event_loop.rs ├── event_loop │ ├── background_thread.rs │ ├── linux.rs │ ├── macos.rs │ └── windows.rs ├── formatters.rs ├── lib.rs ├── midi.rs ├── midi │ └── sysex.rs ├── params.rs ├── params │ ├── boolean.rs │ ├── enums.rs │ ├── float.rs │ ├── integer.rs │ ├── internals.rs │ ├── persist.rs │ ├── range.rs │ └── smoothing.rs ├── plugin.rs ├── plugin │ ├── clap.rs │ └── vst3.rs ├── prelude.rs ├── util.rs ├── util │ ├── stft.rs │ └── window.rs ├── wrapper.rs └── wrapper │ ├── clap.rs │ ├── clap │ ├── context.rs │ ├── descriptor.rs │ ├── features.rs │ ├── util.rs │ └── wrapper.rs │ ├── standalone.rs │ ├── standalone │ ├── backend.rs │ ├── backend │ │ ├── cpal.rs │ │ ├── dummy.rs │ │ └── jack.rs │ ├── config.rs │ ├── context.rs │ └── wrapper.rs │ ├── state.rs │ ├── util.rs │ ├── util │ ├── buffer_management.rs │ └── context_checks.rs │ ├── vst3.rs │ └── vst3 │ ├── context.rs │ ├── factory.rs │ ├── inner.rs │ ├── note_expressions.rs │ ├── param_units.rs │ ├── subcategories.rs │ ├── util.rs │ ├── view.rs │ └── wrapper.rs └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | # Building the xtask package in release mode is normally not necessary, but if 3 | # you're going to compile other plugins in release mode then you'd need to 4 | # recompile serde(-derive) because the xtask packages needs serde to parse the 5 | # `bundler.toml` config file. To avoid needing to compile these expensive crates 6 | # twice, we'll default to also running the xtask target in release mode. 7 | xtask = "run --package xtask --release --" 8 | xtask-debug = "run --package xtask --" 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: robbert-vdh 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://paypal.me/robbertvdh"] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Automated Builds 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | workflow_dispatch: 13 | 14 | defaults: 15 | run: 16 | # This otherwise gets run under dash which does not support brace expansion 17 | shell: bash 18 | 19 | jobs: 20 | # We'll only package the plugins with an entry in bundler.toml 21 | package: 22 | strategy: 23 | matrix: 24 | include: 25 | - { name: ubuntu-22.04, os: ubuntu-22.04, cross-target: '' } 26 | - { name: macos-universal, os: macos-latest, cross-target: x86_64-apple-darwin } 27 | - { name: windows, os: windows-latest, cross-target: '' } 28 | name: Package plugin binaries 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Fetch all git history 33 | run: git fetch --force --prune --tags --unshallow 34 | 35 | - name: Install dependencies 36 | if: startsWith(matrix.os, 'ubuntu') 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y libasound2-dev libgl-dev libjack-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 40 | 41 | - uses: actions/cache@v4 42 | # FIXME: Caching `target/` causes the Windows runner to blow up after some time 43 | if: startsWith(matrix.os, 'windows') 44 | with: 45 | path: | 46 | ~/.cargo/registry/index/ 47 | ~/.cargo/registry/cache/ 48 | ~/.cargo/git/db/ 49 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 50 | - uses: actions/cache@v4 51 | if: "!startsWith(matrix.os, 'windows')" 52 | with: 53 | path: | 54 | ~/.cargo/registry/index/ 55 | ~/.cargo/registry/cache/ 56 | ~/.cargo/git/db/ 57 | target/ 58 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 59 | 60 | - name: Set up Rust toolchain 61 | # Needed for SIMD 62 | uses: dtolnay/rust-toolchain@nightly 63 | with: 64 | # The macOS AArch64 build is done from an x86_64 macOS CI runner, so 65 | # it needs to be cross compiled 66 | targets: ${{ matrix.cross-target }} 67 | - name: Package all targets from bundler.toml 68 | # Instead of hardcoding which targets to build and package, we'll 69 | # package everything that's got en entry in the `bundler.toml` file 70 | run: | 71 | # Building can be sped up by specifying all packages in one go 72 | package_args=() 73 | for package in $(cargo xtask known-packages); do 74 | package_args+=("-p" "$package") 75 | done 76 | 77 | runner_name=${{ matrix.name }} 78 | if [[ $runner_name = 'macos-universal' ]]; then 79 | export MACOSX_DEPLOYMENT_TARGET=10.13 80 | cargo xtask bundle-universal "${package_args[@]}" --release 81 | else 82 | cross_target=${{ matrix.cross-target }} 83 | if [[ -n $cross_target ]]; then 84 | package_args+=("--target" "$cross_target") 85 | fi 86 | 87 | cargo xtask bundle "${package_args[@]}" --release 88 | fi 89 | 90 | - name: Determine build archive name 91 | run: | 92 | # Windows (usually) doesn't like colons in file names 93 | echo "ARCHIVE_NAME=nih-plugs-$(date -u +"%Y-%m-%d-%H%m%S")-${{ matrix.name }}" >> "$GITHUB_ENV" 94 | - name: Move all packaged plugin into a directory 95 | run: | 96 | # GitHub Action strips the top level directory, great, have another one 97 | mkdir -p "$ARCHIVE_NAME/$ARCHIVE_NAME" 98 | mv target/bundled/* "$ARCHIVE_NAME/$ARCHIVE_NAME" 99 | - name: Add an OS-specific readme file with installation instructions 100 | run: cp ".github/workflows/readme-${{ runner.os }}.txt" "$ARCHIVE_NAME/$ARCHIVE_NAME/README.txt" 101 | - uses: actions/upload-artifact@v4 102 | with: 103 | name: ${{ env.ARCHIVE_NAME }} 104 | path: ${{ env.ARCHIVE_NAME }} 105 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | defaults: 9 | run: 10 | # This otherwise gets run under dash which does not support brace expansion 11 | shell: bash 12 | 13 | jobs: 14 | docs: 15 | name: Generate and upload docs 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | # Needed for git-describe to do anything useful 20 | - name: Fetch all git history 21 | run: git fetch --force --prune --tags --unshallow 22 | 23 | - name: Install dependencies 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install -y libasound2-dev libgl-dev libjack-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 27 | 28 | - uses: actions/cache@v4 29 | # FIXME: Caching `target/` causes the Windows runner to blow up after some time 30 | if: startsWith(matrix.os, 'windows') 31 | with: 32 | path: | 33 | ~/.cargo/registry/index/ 34 | ~/.cargo/registry/cache/ 35 | ~/.cargo/git/db/ 36 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 37 | - uses: actions/cache@v4 38 | if: "!startsWith(matrix.os, 'windows')" 39 | with: 40 | path: | 41 | ~/.cargo/registry/index/ 42 | ~/.cargo/registry/cache/ 43 | ~/.cargo/git/db/ 44 | target/ 45 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 46 | 47 | - name: Set up Rust toolchain 48 | # Nightly is needed to document the SIMD feature and for the 49 | # `doc_auto_cfg` feature 50 | uses: dtolnay/rust-toolchain@nightly 51 | - name: Generate documentation for all targets 52 | # Don't use --all-features here as that will enable a whole bunch of 53 | # conflicting iced features. We also don't want to use `--workspace` 54 | # here because that would also document our plugins and binary crates. 55 | run: >- 56 | cargo doc --features docs,simd,standalone,zstd --no-deps 57 | -p nih_plug 58 | -p nih_plug_derive 59 | -p nih_plug_egui 60 | -p nih_plug_iced 61 | -p nih_plug_vizia 62 | -p nih_plug_xtask 63 | 64 | - name: Add a redirect index page 65 | run: | 66 | cat > target/doc/index.html < 68 | 69 | EOF 70 | - name: Deploy to GitHub Pages 71 | uses: JamesIves/github-pages-deploy-action@v4.3.0 72 | with: 73 | branch: gh-pages 74 | folder: target/doc 75 | 76 | # Having the gh-pages branch on this repository adds a whole bunch of 77 | # noise to the GitHub feed if you follow this repo 78 | repository-name: robbert-vdh/nih-plug-docs 79 | ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }} 80 | -------------------------------------------------------------------------------- /.github/workflows/readme-Linux.txt: -------------------------------------------------------------------------------- 1 | To install the VST3 plugins, copy the .vst3 _directories_ to: 2 | ~/.vst3 3 | 4 | To install the CLAP plugins, copy the .clap files to: 5 | ~/.clap 6 | 7 | You will need to create these directories yourself it they do not yet exist. 8 | 9 | See https://github.com/free-audio/clap#hosts for instructions on how to enable 10 | CLAP support in your DAW. 11 | -------------------------------------------------------------------------------- /.github/workflows/readme-Windows.txt: -------------------------------------------------------------------------------- 1 | To install the VST3 plugins, copy the .vst3 _directories_ to: 2 | C:/Program Files/Common Files/VST3/ 3 | 4 | To install the CLAP plugins, copy the .clap files to: 5 | C:/Program Files/Common Files/CLAP/ 6 | 7 | You will need to create these directories yourself it they do not yet exist. 8 | 9 | See https://github.com/free-audio/clap#hosts for instructions on how to enable 10 | CLAP support in your DAW. 11 | -------------------------------------------------------------------------------- /.github/workflows/readme-macOS.txt: -------------------------------------------------------------------------------- 1 | You may need to disable Gatekeeper on macOS to be able to use the plugins: 2 | https://disable-gatekeeper.github.io/ 3 | 4 | --- 5 | 6 | To install the VST3 plugins, copy the .vst3 bundles to: 7 | ~/Library/Audio/Plug-Ins/VST3 8 | 9 | To install the CLAP plugins, copy the .clap bundles to: 10 | ~/Library/Audio/Plug-Ins/CLAP 11 | 12 | You will need to create these directories yourself it they do not yet exist. 13 | 14 | See https://github.com/free-audio/clap#hosts for instructions on how to enable 15 | CLAP support in your DAW. 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | defaults: 10 | run: 11 | # This otherwise gets run under dash which does not support brace expansion 12 | shell: bash 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-22.04, macos-latest, windows-latest] 19 | name: Build and test all components 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | # Needed for git-describe to do anything useful 24 | - name: Fetch all git history 25 | run: git fetch --force --prune --tags --unshallow 26 | 27 | - name: Install dependencies 28 | if: startsWith(matrix.os, 'ubuntu') 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install -y libasound2-dev libgl-dev libjack-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 32 | 33 | - uses: actions/cache@v4 34 | # FIXME: Caching `target/` causes the Windows runner to blow up after some time 35 | if: startsWith(matrix.os, 'windows') 36 | with: 37 | path: | 38 | ~/.cargo/registry/index/ 39 | ~/.cargo/registry/cache/ 40 | ~/.cargo/git/db/ 41 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 42 | - uses: actions/cache@v4 43 | if: "!startsWith(matrix.os, 'windows')" 44 | with: 45 | path: | 46 | ~/.cargo/registry/index/ 47 | ~/.cargo/registry/cache/ 48 | ~/.cargo/git/db/ 49 | target/ 50 | key: ${{ matrix.name }}-${{ matrix.cross-target }} 51 | 52 | - name: Set up Rust toolchain 53 | # Needed for SIMD 54 | uses: dtolnay/rust-toolchain@nightly 55 | - name: Run the tests 56 | # Don't use --all-features as that will enable a whole bunch of 57 | # conflicting iced features. `--locked` ensures that the lockfile is up 58 | # to date. We only really need this in one of the builds. 59 | run: cargo test --locked --workspace --features "simd,standalone,zstd" 60 | 61 | # This makes sure that NIH-plug can be compiled without VST3 support 62 | build-without-vst3: 63 | name: Build NIH-plug without VST3 support 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Fetch all git history 68 | run: git fetch --force --prune --tags --unshallow 69 | 70 | - name: Install dependencies 71 | run: | 72 | sudo apt-get update 73 | sudo apt-get install -y libasound2-dev libgl-dev libjack-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 74 | 75 | - uses: actions/cache@v4 76 | with: 77 | path: | 78 | ~/.cargo/registry/index/ 79 | ~/.cargo/registry/cache/ 80 | ~/.cargo/git/db/ 81 | target/ 82 | key: build-without-vst3-ubuntu 83 | 84 | - name: Set up Rust toolchain 85 | # Needed for SIMD 86 | uses: dtolnay/rust-toolchain@nightly 87 | - name: Run the tests 88 | run: cargo build --no-default-features 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_strings = true 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug" 3 | version = "0.0.0" 4 | edition = "2021" 5 | rust-version = "1.80" 6 | authors = ["Robbert van der Helm "] 7 | license = "ISC" 8 | 9 | keywords = ["audio", "plugin", "vst", "vst3"] 10 | description = "A simple but modern API-agnostic audio plugin framework" 11 | repository = "https://github.com/robbert-vdh/nih-plug" 12 | 13 | [workspace] 14 | resolver = "2" 15 | members = [ 16 | "nih_plug_derive", 17 | "nih_plug_egui", 18 | "nih_plug_iced", 19 | "nih_plug_vizia", 20 | "nih_plug_xtask", 21 | 22 | "cargo_nih_plug", 23 | "xtask", 24 | 25 | "plugins/examples/byo_gui_gl", 26 | "plugins/examples/byo_gui_softbuffer", 27 | "plugins/examples/byo_gui_wgpu", 28 | "plugins/examples/gain", 29 | "plugins/examples/gain_gui_egui", 30 | "plugins/examples/gain_gui_iced", 31 | "plugins/examples/gain_gui_vizia", 32 | "plugins/examples/midi_inverter", 33 | "plugins/examples/poly_mod_synth", 34 | "plugins/examples/sine", 35 | "plugins/examples/stft", 36 | "plugins/examples/sysex", 37 | 38 | "plugins/soft_vacuum", 39 | "plugins/buffr_glitch", 40 | "plugins/crisp", 41 | "plugins/crossover", 42 | "plugins/diopser", 43 | "plugins/loudness_war_winner", 44 | "plugins/puberty_simulator", 45 | "plugins/safety_limiter", 46 | "plugins/spectral_compressor", 47 | ] 48 | 49 | [features] 50 | default = ["vst3"] 51 | 52 | # Enabling this feature will cause the plugin to terminate when allocations 53 | # occur in the processing function during debug builds. Keep in mind that panics 54 | # may also allocate if they use string formatting, so temporarily disabling this 55 | # feature may be necessary when debugging panics in DSP code. 56 | assert_process_allocs = ["dep:assert_no_alloc"] 57 | # Enables an export target for standalone binaries through the 58 | # `nih_export_standalone()` function. Disabled by default as this requires 59 | # building additional dependencies for audio and MIDI handling. 60 | standalone = ["dep:baseview", "dep:clap", "dep:cpal", "dep:jack", "dep:midir", "dep:rtrb"] 61 | # Enables the `nih_export_vst3!()` macro. Enabled by default. This feature 62 | # exists mostly for GPL-compliance reasons, since even if you don't use the VST3 63 | # wrapper you might otherwise still include a couple (unused) symbols from the 64 | # `vst3-sys` crate. 65 | vst3 = ["dep:vst3-sys"] 66 | # Add adapters to the Buffer object for reading the channel data to and from 67 | # `std::simd` vectors. Requires a nightly compiler. 68 | simd = [] 69 | # Compress plugin state using the Zstandard algorithm. Loading uncompressed 70 | # state is still supported so existing state will still load after enabling this 71 | # feature for a plugin, but it can not be disabled again without losing state 72 | # compatibility. 73 | zstd = ["dep:zstd"] 74 | 75 | # Only relevant when generating docs, adds the `doc_auto_cfg` nightly feature 76 | docs = [] 77 | 78 | [dependencies] 79 | nih_plug_derive = { path = "nih_plug_derive" } 80 | 81 | anyhow = "1.0" 82 | anymap3 = "1.0.1" 83 | atomic_float = "0.1" 84 | atomic_refcell = "0.1" 85 | backtrace = "0.3.65" 86 | bitflags = "1.3" 87 | cfg-if = "1.0" 88 | # This supports CLAP 1.2.2 89 | clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } 90 | crossbeam = "0.8" 91 | log = { version = "0.4", features = ["std", "release_max_level_info"] } 92 | midi-consts = "0.1" 93 | nih_log = "0.3.1" 94 | parking_lot = "0.12" 95 | raw-window-handle = "0.5" 96 | serde = { version = "1.0", features = ["derive"] } 97 | serde_json = "1.0" 98 | widestring = "1.0.0-beta.1" 99 | 100 | # Used for the `assert_process_allocs` feature 101 | assert_no_alloc = { git = "https://github.com/robbert-vdh/rust-assert-no-alloc.git", branch = "feature/nested-permit-forbid", features = ["backtrace", "log"], optional = true } 102 | 103 | # Used for the `standalone` feature 104 | # NOTE: OpenGL support is not needed here, but rust-analyzer gets confused when 105 | # some crates do use it and others don't 106 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "579130ecb4f9f315ae52190af42f0ea46aeaa4a2", features = ["opengl"], optional = true } 107 | # All the claps! 108 | clap = { version = "4.1.8", features = ["derive", "wrap_help"], optional = true } 109 | cpal = { version = "0.15", optional = true } 110 | jack = { version = "0.11.4", optional = true } 111 | midir = { version = "0.9.1", optional = true } 112 | rtrb = { version = "0.2.2", optional = true } 113 | 114 | # Used for the `vst3` feature 115 | vst3-sys = { git = "https://github.com/robbert-vdh/vst3-sys.git", branch = "fix/drop-box-from-raw", optional = true } 116 | 117 | # Used for the `zstd` feature 118 | zstd = { version = "0.12.3", optional = true } 119 | 120 | [dev-dependencies] 121 | approx = "0.5.1" 122 | 123 | [target.'cfg(all(target_family = "unix", not(target_os = "macos")))'.dependencies] 124 | libc = "0.2.124" 125 | 126 | [target.'cfg(target_os = "macos")'.dependencies] 127 | objc = "0.2.7" 128 | core-foundation = "0.9.3" 129 | 130 | [target.'cfg(target_os = "windows")'.dependencies.windows] 131 | version = "0.44" 132 | features = [ 133 | "Win32_Foundation", 134 | "Win32_Graphics_Gdi", 135 | "Win32_UI_WindowsAndMessaging", 136 | "Win32_System_LibraryLoader", 137 | "Win32_System_Performance", 138 | ] 139 | 140 | [profile.release] 141 | lto = "thin" 142 | strip = "symbols" 143 | 144 | [profile.profiling] 145 | inherits = "release" 146 | debug = true 147 | strip = "none" 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022-2024 Robbert van der Helm 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /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 | [soft_vacuum] 8 | name = "Soft Vacuum" 9 | 10 | [buffr_glitch] 11 | name = "Buffr Glitch" 12 | 13 | [crisp] 14 | name = "Crisp" 15 | 16 | [crossover] 17 | name = "Crossover" 18 | 19 | [diopser] 20 | name = "Diopser" 21 | 22 | [loudness_war_winner] 23 | name = "Loudness War Winner" 24 | 25 | [puberty_simulator] 26 | name = "Puberty Simulator" 27 | 28 | [safety_limiter] 29 | name = "Safety Limiter" 30 | 31 | [spectral_compressor] 32 | name = "Spectral Compressor" 33 | -------------------------------------------------------------------------------- /cargo_nih_plug/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-nih-plug" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | description = "A cargo subcommand for compiling and bundling plugins, analogous to NIH-plug's `cargo xtask`" 7 | license = "ISC" 8 | 9 | [dependencies] 10 | nih_plug_xtask = { path = "../nih_plug_xtask" } 11 | -------------------------------------------------------------------------------- /cargo_nih_plug/README.md: -------------------------------------------------------------------------------- 1 | # NIH-plug: cargo subcommand for bundling plugins 2 | 3 | This is NIH-plug's `cargo xtask` command, but as a `cargo` subcommand. This way 4 | you can use it outside of NIH-plug projects. If you're using NIH-plug, you'll 5 | want to use the xtask integration directly instead so you don't need to worry 6 | about keeping the command up to date, see: 7 | . 8 | 9 | Since this has not yet been published to `crates.io`, you'll need to install 10 | this using: 11 | 12 | ```shell 13 | cargo install --git https://github.com/robbert-vdh/nih-plug.git cargo-nih-plug 14 | ``` 15 | 16 | Once that's installed, you can compile and bundle plugins using: 17 | 18 | ```shell 19 | cargo nih-plug bundle --release 20 | ``` 21 | -------------------------------------------------------------------------------- /cargo_nih_plug/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> nih_plug_xtask::Result<()> { 2 | // This includes both the `cargo` command and the `nih-plug` subcommand, so we should get rid of 3 | // those first 4 | let args = std::env::args().skip(2); 5 | nih_plug_xtask::main_with_args("cargo nih-plug", args) 6 | } 7 | -------------------------------------------------------------------------------- /nih_plug_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug_derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | syn = { version = "1.0", features = ["extra-traits"] } 13 | quote = "1.0" 14 | proc-macro2 = "1.0" 15 | 16 | [dev-dependencies] 17 | nih_plug = { path = "..", default-features = false } 18 | -------------------------------------------------------------------------------- /nih_plug_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod enums; 4 | mod params; 5 | 6 | /// Derive the `Enum` trait for simple enum parameters. See `EnumParam` for more information. 7 | #[proc_macro_derive(Enum, attributes(name, id))] 8 | pub fn derive_enum(input: TokenStream) -> TokenStream { 9 | enums::derive_enum(input) 10 | } 11 | 12 | /// Derive the `Params` trait for your plugin's parameters struct. See the `Plugin` trait. 13 | #[proc_macro_derive(Params, attributes(id, persist, nested))] 14 | pub fn derive_params(input: TokenStream) -> TokenStream { 15 | params::derive_params(input) 16 | } 17 | -------------------------------------------------------------------------------- /nih_plug_derive/tests/persist.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::sync::Mutex; 3 | 4 | use nih_plug::prelude::*; 5 | 6 | #[derive(Params, Default)] 7 | struct WrapperParams { 8 | #[nested(id_prefix = "foo")] 9 | pub inner: InnerParams, 10 | } 11 | 12 | #[derive(Params, Default)] 13 | struct ArrayWrapperParams { 14 | #[nested(array)] 15 | pub inners: [InnerParams; 3], 16 | } 17 | 18 | #[derive(Default)] 19 | struct InnerParams { 20 | /// The value `deserialize()` has been called with so we can check that the prefix has been 21 | /// stripped correctly. 22 | pub deserialize_called_with: Mutex>>, 23 | } 24 | 25 | unsafe impl Params for InnerParams { 26 | fn param_map(&self) -> Vec<(String, ParamPtr, String)> { 27 | Vec::new() 28 | } 29 | 30 | fn serialize_fields(&self) -> BTreeMap { 31 | // When nested in another struct, the ID prefix will be added to `bar` 32 | let mut data = BTreeMap::new(); 33 | data.insert(String::from("bar"), String::from("baz")); 34 | 35 | data 36 | } 37 | 38 | fn deserialize_fields(&self, serialized: &BTreeMap) { 39 | *self.deserialize_called_with.lock().unwrap() = Some(serialized.clone()); 40 | } 41 | } 42 | 43 | mod persist { 44 | mod nested_prefix { 45 | 46 | use super::super::*; 47 | 48 | #[test] 49 | fn serialize() { 50 | let params = WrapperParams::default(); 51 | 52 | // This should have had a prefix added to the serialized value 53 | let serialized = params.serialize_fields(); 54 | assert_eq!(serialized.len(), 1); 55 | assert_eq!(serialized["foo_bar"], "baz"); 56 | } 57 | 58 | #[test] 59 | fn deserialize() { 60 | let mut serialized = BTreeMap::new(); 61 | serialized.insert(String::from("foo_bar"), String::from("aaa")); 62 | 63 | let params = WrapperParams::default(); 64 | params.deserialize_fields(&serialized); 65 | 66 | // This contains the values passed to the inner struct's deserialize function 67 | let deserialized = params 68 | .inner 69 | .deserialize_called_with 70 | .lock() 71 | .unwrap() 72 | .take() 73 | .unwrap(); 74 | assert_eq!(deserialized.len(), 1); 75 | assert_eq!(deserialized["bar"], "aaa"); 76 | } 77 | 78 | #[test] 79 | fn deserialize_mismatching_prefix() { 80 | let mut serialized = BTreeMap::new(); 81 | serialized.insert(String::from("foo_bar"), String::from("aaa")); 82 | serialized.insert( 83 | String::from("something"), 84 | String::from("this should not be there"), 85 | ); 86 | 87 | let params = WrapperParams::default(); 88 | params.deserialize_fields(&serialized); 89 | 90 | // The `something` key should not be passed to the child struct 91 | let deserialized = params 92 | .inner 93 | .deserialize_called_with 94 | .lock() 95 | .unwrap() 96 | .take() 97 | .unwrap(); 98 | assert_eq!(deserialized.len(), 1); 99 | assert_eq!(deserialized["bar"], "aaa"); 100 | } 101 | } 102 | 103 | mod array_suffix { 104 | use super::super::*; 105 | 106 | #[test] 107 | fn serialize() { 108 | let params = ArrayWrapperParams::default(); 109 | 110 | let serialized = params.serialize_fields(); 111 | assert_eq!(serialized.len(), 3); 112 | assert_eq!(serialized["bar_1"], "baz"); 113 | assert_eq!(serialized["bar_2"], "baz"); 114 | assert_eq!(serialized["bar_2"], "baz"); 115 | } 116 | 117 | #[test] 118 | fn deserialize() { 119 | let mut serialized = BTreeMap::new(); 120 | serialized.insert(String::from("bar_1"), String::from("aaa")); 121 | serialized.insert(String::from("bar_2"), String::from("bbb")); 122 | serialized.insert(String::from("bar_3"), String::from("ccc")); 123 | 124 | let params = ArrayWrapperParams::default(); 125 | params.deserialize_fields(&serialized); 126 | for (inner, expected_value) in params.inners.into_iter().zip(["aaa", "bbb", "ccc"]) { 127 | let deserialized = inner 128 | .deserialize_called_with 129 | .lock() 130 | .unwrap() 131 | .take() 132 | .unwrap(); 133 | assert_eq!(deserialized.len(), 1); 134 | assert_eq!(deserialized["bar"], expected_value); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /nih_plug_egui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug_egui" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "An adapter to use egui GUIs with NIH-plug" 9 | 10 | [features] 11 | default = ["opengl", "default_fonts"] 12 | # `nih_plug_egui` always uses OpenGL since egui's wgpu backend is still unstable 13 | # depending on the platform 14 | opengl = ["egui-baseview/opengl"] 15 | default_fonts = ["egui-baseview/default_fonts"] 16 | rayon = ["egui-baseview/rayon"] 17 | 18 | [dependencies] 19 | nih_plug = { path = "..", default-features = false } 20 | raw-window-handle = "0.5" 21 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "9a0b42c09d712777b2edb4c5e0cb6baf21e988f0" } 22 | crossbeam = "0.8" 23 | egui-baseview = { git = "https://github.com/BillyDM/egui-baseview.git", rev = "ec70c3fe6b2f070dcacbc22924431edbe24bd1c0" } 24 | parking_lot = "0.12" 25 | # To make the state persistable 26 | serde = { version = "1.0", features = ["derive"] } 27 | -------------------------------------------------------------------------------- /nih_plug_egui/README.md: -------------------------------------------------------------------------------- 1 | # NIH-plug: egui support 2 | 3 | This provides an adapter to create [egui](https://github.com/emilk/egui) based 4 | GUIs with NIH-plug through 5 | [egui-baseview](https://github.com/BillyDM/egui-baseview). 6 | 7 | Consider using [`nih_plug_iced`](../nih_plug_iced) or 8 | [`nih_plug_vizia`](../nih_plug_vizia) instead. 9 | -------------------------------------------------------------------------------- /nih_plug_egui/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [egui](https://github.com/emilk/egui) editor support for NIH plug. 2 | //! 3 | //! TODO: Proper usage example, for now check out the gain_gui example 4 | 5 | // See the comment in the main `nih_plug` crate 6 | #![allow(clippy::type_complexity)] 7 | 8 | use crossbeam::atomic::AtomicCell; 9 | use egui::Context; 10 | use nih_plug::params::persist::PersistentField; 11 | use nih_plug::prelude::{Editor, ParamSetter}; 12 | use parking_lot::RwLock; 13 | use serde::{Deserialize, Serialize}; 14 | use std::sync::atomic::{AtomicBool, Ordering}; 15 | use std::sync::Arc; 16 | 17 | #[cfg(not(feature = "opengl"))] 18 | compile_error!("There's currently no software rendering support for egui"); 19 | 20 | /// Re-export for convenience. 21 | pub use egui_baseview::egui; 22 | 23 | mod editor; 24 | pub mod resizable_window; 25 | pub mod widgets; 26 | 27 | /// Create an [`Editor`] instance using an [`egui`][::egui] GUI. Using the user state parameter is 28 | /// optional, but it can be useful for keeping track of some temporary GUI-only settings. See the 29 | /// `gui_gain` example for more information on how to use this. The [`EguiState`] passed to this 30 | /// function contains the GUI's intitial size, and this is kept in sync whenever the GUI gets 31 | /// resized. You can also use this to know if the GUI is open, so you can avoid performing 32 | /// potentially expensive calculations while the GUI is not open. If you want this size to be 33 | /// persisted when restoring a plugin instance, then you can store it in a `#[persist = "key"]` 34 | /// field on your parameters struct. 35 | /// 36 | /// See [`EguiState::from_size()`]. 37 | pub fn create_egui_editor( 38 | egui_state: Arc, 39 | user_state: T, 40 | build: B, 41 | update: U, 42 | ) -> Option> 43 | where 44 | T: 'static + Send + Sync, 45 | B: Fn(&Context, &mut T) + 'static + Send + Sync, 46 | U: Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync, 47 | { 48 | Some(Box::new(editor::EguiEditor { 49 | egui_state, 50 | user_state: Arc::new(RwLock::new(user_state)), 51 | build: Arc::new(build), 52 | update: Arc::new(update), 53 | 54 | // TODO: We can't get the size of the window when baseview does its own scaling, so if the 55 | // host does not set a scale factor on Windows or Linux we should just use a factor of 56 | // 1. That may make the GUI tiny but it also prevents it from getting cut off. 57 | #[cfg(target_os = "macos")] 58 | scaling_factor: AtomicCell::new(None), 59 | #[cfg(not(target_os = "macos"))] 60 | scaling_factor: AtomicCell::new(Some(1.0)), 61 | })) 62 | } 63 | 64 | /// State for an `nih_plug_egui` editor. 65 | #[derive(Debug, Serialize, Deserialize)] 66 | pub struct EguiState { 67 | /// The window's size in logical pixels before applying `scale_factor`. 68 | #[serde(with = "nih_plug::params::persist::serialize_atomic_cell")] 69 | size: AtomicCell<(u32, u32)>, 70 | 71 | /// The new size of the window, if it was requested to resize by the GUI. 72 | #[serde(skip)] 73 | requested_size: AtomicCell>, 74 | 75 | /// Whether the editor's window is currently open. 76 | #[serde(skip)] 77 | open: AtomicBool, 78 | } 79 | 80 | impl<'a> PersistentField<'a, EguiState> for Arc { 81 | fn set(&self, new_value: EguiState) { 82 | self.size.store(new_value.size.load()); 83 | } 84 | 85 | fn map(&self, f: F) -> R 86 | where 87 | F: Fn(&EguiState) -> R, 88 | { 89 | f(self) 90 | } 91 | } 92 | 93 | impl EguiState { 94 | /// Initialize the GUI's state. This value can be passed to [`create_egui_editor()`]. The window 95 | /// size is in logical pixels, so before it is multiplied by the DPI scaling factor. 96 | pub fn from_size(width: u32, height: u32) -> Arc { 97 | Arc::new(EguiState { 98 | size: AtomicCell::new((width, height)), 99 | requested_size: Default::default(), 100 | open: AtomicBool::new(false), 101 | }) 102 | } 103 | 104 | /// Returns a `(width, height)` pair for the current size of the GUI in logical pixels. 105 | pub fn size(&self) -> (u32, u32) { 106 | self.size.load() 107 | } 108 | 109 | /// Whether the GUI is currently visible. 110 | // Called `is_open()` instead of `open()` to avoid the ambiguity. 111 | pub fn is_open(&self) -> bool { 112 | self.open.load(Ordering::Acquire) 113 | } 114 | 115 | /// Set the new size that will be used to resize the window if the host allows. 116 | fn set_requested_size(&self, new_size: (u32, u32)) { 117 | self.requested_size.store(Some(new_size)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /nih_plug_egui/src/resizable_window.rs: -------------------------------------------------------------------------------- 1 | //! Resizable window wrapper for Egui editor. 2 | 3 | use egui_baseview::egui::emath::GuiRounding; 4 | use egui_baseview::egui::{InnerResponse, UiBuilder}; 5 | 6 | use crate::egui::{pos2, CentralPanel, Context, Id, Rect, Response, Sense, Ui, Vec2}; 7 | use crate::EguiState; 8 | 9 | /// Adds a corner to the plugin window that can be dragged in order to resize it. 10 | /// Resizing happens through plugin API, hence a custom implementation is needed. 11 | pub struct ResizableWindow { 12 | id: Id, 13 | min_size: Vec2, 14 | } 15 | 16 | impl ResizableWindow { 17 | pub fn new(id_source: impl std::hash::Hash) -> Self { 18 | Self { 19 | id: Id::new(id_source), 20 | min_size: Vec2::splat(16.0), 21 | } 22 | } 23 | 24 | /// Won't shrink to smaller than this 25 | #[inline] 26 | pub fn min_size(mut self, min_size: impl Into) -> Self { 27 | self.min_size = min_size.into(); 28 | self 29 | } 30 | 31 | pub fn show( 32 | self, 33 | context: &Context, 34 | egui_state: &EguiState, 35 | add_contents: impl FnOnce(&mut Ui) -> R, 36 | ) -> InnerResponse { 37 | CentralPanel::default().show(context, move |ui| { 38 | let ui_rect = ui.clip_rect(); 39 | let mut content_ui = 40 | ui.new_child(UiBuilder::new().max_rect(ui_rect).layout(*ui.layout())); 41 | 42 | let ret = add_contents(&mut content_ui); 43 | 44 | let corner_size = Vec2::splat(ui.visuals().resize_corner_size); 45 | let corner_rect = Rect::from_min_size(ui_rect.max - corner_size, corner_size); 46 | 47 | let corner_response = ui.interact(corner_rect, self.id.with("corner"), Sense::drag()); 48 | 49 | if let Some(pointer_pos) = corner_response.interact_pointer_pos() { 50 | let desired_size = (pointer_pos - ui_rect.min + 0.5 * corner_response.rect.size()) 51 | .max(self.min_size); 52 | 53 | if corner_response.dragged() { 54 | egui_state.set_requested_size(( 55 | desired_size.x.round() as u32, 56 | desired_size.y.round() as u32, 57 | )); 58 | } 59 | } 60 | 61 | paint_resize_corner(&content_ui, &corner_response); 62 | 63 | ret 64 | }) 65 | } 66 | } 67 | 68 | pub fn paint_resize_corner(ui: &Ui, response: &Response) { 69 | let stroke = ui.style().interact(response).fg_stroke; 70 | 71 | let painter = ui.painter(); 72 | let rect = response.rect.translate(-Vec2::splat(2.0)); // move away from the corner 73 | let cp = rect.max.round_to_pixels(painter.pixels_per_point()); 74 | 75 | let mut w = 2.0; 76 | 77 | while w <= rect.width() && w <= rect.height() { 78 | painter.line_segment([pos2(cp.x - w, cp.y), pos2(cp.x, cp.y - w)], stroke); 79 | w += 4.0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nih_plug_egui/src/widgets.rs: -------------------------------------------------------------------------------- 1 | //! Custom egui widgets for displaying parameter values. 2 | //! 3 | //! # Note 4 | //! 5 | //! None of these widgets are finalized, and their sizes or looks can change at any point. Feel free 6 | //! to copy the widgets and modify them to your personal taste. 7 | 8 | pub mod generic_ui; 9 | mod param_slider; 10 | pub mod util; 11 | 12 | pub use param_slider::ParamSlider; 13 | -------------------------------------------------------------------------------- /nih_plug_egui/src/widgets/generic_ui.rs: -------------------------------------------------------------------------------- 1 | //! A simple generic UI widget that renders all parameters in a [`Params`] object as a scrollable 2 | //! list of sliders and labels. 3 | 4 | use std::sync::Arc; 5 | 6 | use egui_baseview::egui::{self, TextStyle, Ui, Vec2}; 7 | use nih_plug::prelude::{Param, ParamFlags, ParamPtr, ParamSetter, Params}; 8 | 9 | use super::ParamSlider; 10 | 11 | /// A widget that can be used to create a generic UI with. This is used in conjuction with empty 12 | /// structs to emulate existential types. 13 | pub trait ParamWidget { 14 | fn add_widget(&self, ui: &mut Ui, param: &P, setter: &ParamSetter); 15 | 16 | /// The same as [`add_widget()`][Self::add_widget()], but for a `ParamPtr`. 17 | /// 18 | /// # Safety 19 | /// 20 | /// Undefined behavior of the `ParamPtr` does not point to a valid parameter. 21 | unsafe fn add_widget_raw(&self, ui: &mut Ui, param: &ParamPtr, setter: &ParamSetter) { 22 | match param { 23 | ParamPtr::FloatParam(p) => self.add_widget(ui, &**p, setter), 24 | ParamPtr::IntParam(p) => self.add_widget(ui, &**p, setter), 25 | ParamPtr::BoolParam(p) => self.add_widget(ui, &**p, setter), 26 | ParamPtr::EnumParam(p) => self.add_widget(ui, &**p, setter), 27 | } 28 | } 29 | } 30 | 31 | /// Create a generic UI using [`ParamSlider`]s. 32 | pub struct GenericSlider; 33 | 34 | /// Create a scrollable generic UI using the specified widget. Takes up all the remaining vertical 35 | /// space. 36 | pub fn create( 37 | ui: &mut Ui, 38 | params: Arc, 39 | setter: &ParamSetter, 40 | widget: impl ParamWidget, 41 | ) { 42 | let padding = Vec2::splat(ui.text_style_height(&TextStyle::Body) * 0.2); 43 | egui::containers::ScrollArea::vertical() 44 | // Take up all remaining space, use a wrapper container to adjust how much space that is 45 | .auto_shrink([false, false]) 46 | .show(ui, |ui| { 47 | let mut first_widget = true; 48 | for (_, param_ptr, _) in params.param_map().into_iter() { 49 | let flags = unsafe { param_ptr.flags() }; 50 | if flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) { 51 | continue; 52 | } 53 | 54 | // This list looks weird without a little padding 55 | if !first_widget { 56 | ui.allocate_space(padding); 57 | } 58 | 59 | ui.label(unsafe { param_ptr.name() }); 60 | unsafe { widget.add_widget_raw(ui, ¶m_ptr, setter) }; 61 | 62 | first_widget = false; 63 | } 64 | }); 65 | } 66 | 67 | impl ParamWidget for GenericSlider { 68 | fn add_widget(&self, ui: &mut Ui, param: &P, setter: &ParamSetter) { 69 | // Make these sliders a bit wider, else they look a bit odd 70 | ui.add(ParamSlider::for_param(param, setter).with_width(100.0)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /nih_plug_egui/src/widgets/util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for creating these widgets. 2 | 3 | use egui_baseview::egui::{self, Color32}; 4 | 5 | /// Additively modify the hue, saturation, and lightness [0, 1] values of a color. 6 | pub fn add_hsv(color: Color32, h: f32, s: f32, v: f32) -> Color32 { 7 | let mut hsv = egui::epaint::Hsva::from(color); 8 | hsv.h += h; 9 | hsv.s += s; 10 | hsv.v += v; 11 | hsv.into() 12 | } 13 | 14 | /// Multiplicatively modify the hue, saturation, and lightness [0, 1] values of a color. 15 | pub fn scale_hsv(color: Color32, h: f32, s: f32, v: f32) -> Color32 { 16 | let mut hsv = egui::epaint::Hsva::from(color); 17 | hsv.h *= h; 18 | hsv.s *= s; 19 | hsv.v *= v; 20 | hsv.into() 21 | } 22 | -------------------------------------------------------------------------------- /nih_plug_iced/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug_iced" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "An adapter to use iced GUIs with NIH-plug" 9 | 10 | [features] 11 | default = ["opengl"] 12 | 13 | # NOTE: wgpu support has been removed from the iced-baseview fork out because 14 | # this old iced version uses a wgpu version that doesn't pin the web-sys 15 | # version it uses, and web-sys broke semver 16 | # # Use wgpu rendering, which translates to Vulkan, Metal, or Direct3D12 depending 17 | # # on the platform. 18 | # # NOTE: The OpenGL support in baseview is not used, this is just a workaround 19 | # # for a rust analyzer bug. See the comment in lib.rs. 20 | # wgpu = ["iced_baseview/wgpu", "baseview/opengl"] 21 | # Use OpenGL instead of wgpu for the rendering. This should increase platform 22 | # compatibility at the cost of some iced features not being available. 23 | opengl = ["iced_baseview/glow"] 24 | 25 | # Enables a debug view in native platforms (press F12) 26 | debug = ["iced_baseview/debug"] 27 | 28 | # # Enables the `Image` widget, only supported by the wgpu backend 29 | # wgpu_image = ["iced_baseview/wgpu_image"] 30 | # # Enables the `Svg` widget, only supported by the wgpu backend 31 | # wgpu_svg = ["iced_baseview/wgpu_svg"] 32 | 33 | # # Enables the `Canvas` widget for the wgpu backend 34 | # wgpu_canvas = ["iced_baseview/wgpu_canvas"] 35 | # Enables the `Canvas` widget for the OpenGL backend 36 | opengl_canvas = ["iced_baseview/glow_canvas"] 37 | 38 | # # Enables the `QRCode` widget for the wgpu backend 39 | # wgpu_qr_code = ["iced_baseview/wgpu_qr_code"] 40 | # Enables the `QRCode` widget for the OpenGL backend 41 | opengl_qr_code = ["iced_baseview/glow_qr_code"] 42 | 43 | # # Enables using system fonts for the wgpu backend 44 | # wgpu_default_system_font = ["iced_baseview/wgpu_default_system_font"] 45 | # Enables using system fonts for the OpenGL backend 46 | opengl_default_system_font = ["iced_baseview/glow_default_system_font"] 47 | 48 | # Enables advanced color conversion via `palette` 49 | palette = ["iced_baseview/palette"] 50 | 51 | # Enables `tokio` as the `executor::Default` on native platforms 52 | tokio = ["iced_baseview/tokio"] 53 | # Enables `async-std` as the `executor::Default` on native platforms 54 | async-std = ["iced_baseview/async-std"] 55 | # Enables `smol` as the `executor::Default` on native platforms 56 | smol = ["iced_baseview/smol"] 57 | 58 | [dependencies] 59 | nih_plug = { path = "..", default-features = false } 60 | nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" } 61 | 62 | # The currently targeted version of baseview uses a different version of 63 | # `raw_window_handle` than NIH-plug, so we need to manually convert between them 64 | raw-window-handle = "0.4" 65 | 66 | atomic_refcell = "0.1" 67 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "1d9806d5bd92275d0d8142d9c9c90198757b9b25" } 68 | crossbeam = "0.8" 69 | # This targets iced 0.4 70 | iced_baseview = { git = "https://github.com/robbert-vdh/iced_baseview.git", branch = "feature/update-baseview", default-features = false } 71 | # To make the state persistable 72 | serde = { version = "1.0", features = ["derive"] } 73 | -------------------------------------------------------------------------------- /nih_plug_iced/README.md: -------------------------------------------------------------------------------- 1 | # NIH-plug: iced support 2 | 3 | This provides an adapter to create [iced](https://github.com/iced-rs/iced) based 4 | GUIs with NIH-plug through 5 | [iced_baseview](https://github.com/BillyDM/iced_baseview). 6 | 7 | By default this targets OpenGL as wgpu causes segfaults on a number of 8 | configurations. To use wgpu instead, include the crate with the following 9 | options: 10 | 11 | ```toml 12 | nih_plug_iced = { git = "https://github.com/robbert-vdh/nih-plug.git", default-features = false, features = ["wgpu"] } 13 | ``` 14 | 15 | Iced has many more optional features. Check the `Cargo.toml` file for more 16 | information. 17 | -------------------------------------------------------------------------------- /nih_plug_iced/src/assets.rs: -------------------------------------------------------------------------------- 1 | //! Binary assets for use with `nih_plug_iced`. 2 | 3 | use crate::Font; 4 | 5 | // This module provides a re-export and simple font wrappers around the re-exported fonts. 6 | pub use nih_plug_assets::*; 7 | 8 | pub const NOTO_SANS_REGULAR: Font = Font::External { 9 | name: "Noto Sans Regular", 10 | bytes: fonts::NOTO_SANS_REGULAR, 11 | }; 12 | 13 | pub const NOTO_SANS_REGULAR_ITALIC: Font = Font::External { 14 | name: "Noto Sans Regular Italic", 15 | bytes: fonts::NOTO_SANS_REGULAR_ITALIC, 16 | }; 17 | 18 | pub const NOTO_SANS_THIN: Font = Font::External { 19 | name: "Noto Sans Thin", 20 | bytes: fonts::NOTO_SANS_THIN, 21 | }; 22 | 23 | pub const NOTO_SANS_THIN_ITALIC: Font = Font::External { 24 | name: "Noto Sans Thin Italic", 25 | bytes: fonts::NOTO_SANS_THIN_ITALIC, 26 | }; 27 | 28 | pub const NOTO_SANS_LIGHT: Font = Font::External { 29 | name: "Noto Sans Light", 30 | bytes: fonts::NOTO_SANS_LIGHT, 31 | }; 32 | 33 | pub const NOTO_SANS_LIGHT_ITALIC: Font = Font::External { 34 | name: "Noto Sans Light Italic", 35 | bytes: fonts::NOTO_SANS_LIGHT_ITALIC, 36 | }; 37 | 38 | pub const NOTO_SANS_BOLD: Font = Font::External { 39 | name: "Noto Sans Bold", 40 | bytes: fonts::NOTO_SANS_BOLD, 41 | }; 42 | 43 | pub const NOTO_SANS_BOLD_ITALIC: Font = Font::External { 44 | name: "Noto Sans Bold Italic", 45 | bytes: fonts::NOTO_SANS_BOLD_ITALIC, 46 | }; 47 | -------------------------------------------------------------------------------- /nih_plug_iced/src/widgets.rs: -------------------------------------------------------------------------------- 1 | //! Widgets and utilities for making widgets to integrate iced with NIH-plug. 2 | //! 3 | //! # Note 4 | //! 5 | //! None of these widgets are finalized, and their sizes or looks can change at any point. Feel free 6 | //! to copy the widgets and modify them to your personal taste. 7 | 8 | use nih_plug::prelude::ParamPtr; 9 | 10 | pub mod generic_ui; 11 | pub mod param_slider; 12 | pub mod peak_meter; 13 | pub mod util; 14 | 15 | pub use param_slider::ParamSlider; 16 | pub use peak_meter::PeakMeter; 17 | 18 | /// A message to update a parameter value. Since NIH-plug manages the parameters, interacting with 19 | /// parameter values with iced works a little different from updating any other state. This main 20 | /// [`IcedEditor`][super::IcedEditor] should have a [`Message`][super::IcedEditor::Message] variant 21 | /// containing this `ParamMessage`. When it receives one of those messages, it can pass it through 22 | /// to [`self.handle_param_message()`][super::IcedEditor::handle_param_message]. 23 | #[derive(Debug, Clone, Copy)] 24 | pub enum ParamMessage { 25 | /// Begin an automation gesture for a parameter. 26 | BeginSetParameter(ParamPtr), 27 | /// Set a parameter to a new normalized value. This needs to be surrounded by a matching 28 | /// `BeginSetParameter` and `EndSetParameter`. 29 | SetParameterNormalized(ParamPtr, f32), 30 | /// End an automation gesture for a parameter. 31 | EndSetParameter(ParamPtr), 32 | } 33 | -------------------------------------------------------------------------------- /nih_plug_iced/src/widgets/util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for creating these widgets. 2 | 3 | use crate::Rectangle; 4 | 5 | /// Remap a `[0, 1]` value to an x-coordinate within this rectangle. The value will be clamped to 6 | /// `[0, 1]` if it isn't already in that range. 7 | pub fn remap_rect_x_t(rect: &Rectangle, t: f32) -> f32 { 8 | rect.x + (rect.width * t.clamp(0.0, 1.0)) 9 | } 10 | 11 | /// Remap a `[0, 1]` value to a y-coordinate within this rectangle. The value will be clamped to 12 | /// `[0, 1]` if it isn't already in that range. 13 | pub fn remap_rect_y_t(rect: &Rectangle, t: f32) -> f32 { 14 | rect.y + (rect.height * t.clamp(0.0, 1.0)) 15 | } 16 | 17 | /// Remap an x-coordinate to a `[0, 1]` value within this rectangle. The value will be clamped to 18 | /// `[0, 1]` if it isn't already in that range. 19 | pub fn remap_rect_x_coordinate(rect: &Rectangle, x_coord: f32) -> f32 { 20 | ((x_coord - rect.x) / rect.width).clamp(0.0, 1.0) 21 | } 22 | 23 | /// Remap a y-coordinate to a `[0, 1]` value within this rectangle. The value will be clamped to 24 | /// `[0, 1]` if it isn't already in that range. 25 | pub fn remap_rect_y_coordinate(rect: &Rectangle, y_coord: f32) -> f32 { 26 | ((y_coord - rect.y) / rect.height).clamp(0.0, 1.0) 27 | } 28 | -------------------------------------------------------------------------------- /nih_plug_vizia/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug_vizia" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "An adapter to use VIZIA GUIs with NIH-plug" 9 | 10 | [dependencies] 11 | nih_plug = { path = "..", default-features = false } 12 | nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" } 13 | 14 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "2c1b1a7b0fef1a29a5150a6a8f6fef6a0cbab8c4" } 15 | # This contains an as of writing not yet merged patch for rounding errors when 16 | # resizing, and a workaround for certain events not firing when resizing 17 | vizia = { git = "https://github.com/robbert-vdh/vizia.git", tag = "patched-2024-05-06", default-features = false, features = ["baseview", "clipboard", "x11"] } 18 | 19 | crossbeam = "0.8" 20 | # To make the state persistable 21 | serde = { version = "1.0", features = ["derive"] } 22 | -------------------------------------------------------------------------------- /nih_plug_vizia/README.md: -------------------------------------------------------------------------------- 1 | # NIH-plug: VIZIA support 2 | 3 | This provides an adapter to create [VIZIA](https://github.com/geom3trik/VIZIA) 4 | based GUIs with NIH-plug. See the documentation in the main 5 | `create_vizia_editor()` function and the examples in the repo for pointers on 6 | how this integrates VIZIA with NIH-plug. 7 | -------------------------------------------------------------------------------- /nih_plug_vizia/assets/theme.css: -------------------------------------------------------------------------------- 1 | /* Overrides for default VIZIA widgets */ 2 | 3 | :root { 4 | background-color: #fafafa; 5 | color: #0a0a0a; 6 | font-size: 15; 7 | /* 8 | * NOTE: vizia's font rendering looks way too dark and thick. Going one font 9 | * weight lower seems to compensate for this. 10 | */ 11 | font-weight: light; 12 | } 13 | 14 | scrollview > scroll_content { 15 | /* Normally the scroll bar overlaps with the content, so we'll add a little offset to prevent that */ 16 | child-right: 15px; 17 | } 18 | scrollview scrollbar { 19 | background-color: #dadada; 20 | border-radius: 0px; 21 | child-space: 0px; 22 | } 23 | scrollview scrollbar.horizontal { 24 | right: 10px; 25 | height: 10px; 26 | } 27 | scrollview scrollbar.vertical { 28 | width: 10px; 29 | } 30 | scrollview scrollbar .thumb { 31 | background-color: #5d5d5d; 32 | border-radius: 0px; 33 | min-width: 10px; 34 | min-height: 10px; 35 | transition: background-color 100ms; 36 | } 37 | scrollview scrollbar .thumb:hover { 38 | background-color: #808080; 39 | transition: background-color 100ms; 40 | } 41 | -------------------------------------------------------------------------------- /nih_plug_vizia/assets/widgets.css: -------------------------------------------------------------------------------- 1 | /* Default styling for the widgets included in nih_plug_vizia */ 2 | /* See ./theme.css for overrides for the default widgets */ 3 | 4 | generic-ui { 5 | child-space: 10px; 6 | col-between: 5px; 7 | /* Otherwise it doesn't interact correctly with scrollview */ 8 | height: auto; 9 | layout-type: column; 10 | row-between: 5px; 11 | } 12 | scrollview > scroll_content > generic-ui { 13 | child-space: 5px; 14 | 15 | /* Our scrollview styling will already add some padding here */ 16 | child-right: 0px; 17 | } 18 | 19 | generic-ui .row { 20 | col-between: 6px; 21 | height: auto; 22 | layout-type: row; 23 | } 24 | 25 | generic-ui .label { 26 | left: 1s; 27 | top: 1s; 28 | bottom: 1s; 29 | } 30 | 31 | param-button { 32 | height: 30px; 33 | width: auto; 34 | border-color: #0a0a0a; 35 | border-width: 1px; 36 | child-top: 1s; 37 | child-right: 7px; 38 | child-bottom: 1s; 39 | child-left: 7px; 40 | background-color: #d0d0d000; 41 | transition: background-color 100ms; 42 | } 43 | param-button:hover { 44 | background-color: #d0d0d080; 45 | transition: background-color 100ms; 46 | } 47 | param-button:checked { 48 | background-color: #d0d0d0; 49 | transition: background-color 100ms; 50 | } 51 | 52 | param-button.bypass { 53 | background-color: #ffcfcb00; 54 | transition: background-color 100ms; 55 | } 56 | param-button.bypass:hover { 57 | background-color: #ffcfcb20; 58 | transition: background-color 100ms; 59 | } 60 | param-button.bypass:checked { 61 | background-color: #ffcfcb; 62 | transition: background-color 100ms; 63 | } 64 | 65 | param-slider { 66 | height: 30px; 67 | width: 180px; 68 | border-color: #0a0a0a; 69 | border-width: 1px; 70 | background-color: transparent; 71 | transition: background-color 100ms; 72 | } 73 | 74 | param-slider:active, 75 | param-slider:hover { 76 | background-color: #8080801a; 77 | transition: background-color 100ms; 78 | } 79 | 80 | param-slider .fill { 81 | background-color: #c4c4c4; 82 | } 83 | param-slider .fill--modulation { 84 | background-color: #a4eafc69; 85 | } 86 | 87 | /* This is a textbox, but we want it to appear just like the label */ 88 | param-slider .value-entry { 89 | /* Vizia doesn't support the unset value */ 90 | background-color: transparent; 91 | border-width: 0px; 92 | child-space: 1s; 93 | } 94 | param-slider .value-entry .caret { 95 | background-color: #0a0a0a; 96 | } 97 | param-slider .value-entry .selection { 98 | background-color: #0a0a0a30; 99 | } 100 | 101 | peak-meter { 102 | height: 30px; 103 | width: 180px; 104 | } 105 | 106 | peak-meter .bar { 107 | height: 50%; 108 | border-width: 1px; 109 | border-color: #0a0a0a; 110 | } 111 | 112 | peak-meter .ticks { 113 | height: 50%; 114 | } 115 | peak-meter .ticks__tick { 116 | background-color: #0a0a0a; 117 | top: 0px; 118 | width: 1px; 119 | height: 30%; 120 | } 121 | peak-meter .ticks__label { 122 | top: 4px; /* In pixels in an attempt to get this to better align to the grid */ 123 | font-size: 11; /* 14.667px */ 124 | } 125 | 126 | resize-handle { 127 | bottom: 0px; 128 | color: #696969; 129 | height: 20px; 130 | left: 1s; 131 | opacity: 0.4; 132 | position-type: self-directed; 133 | right: 0px; 134 | top: 1s; 135 | transition: opacity 100ms; 136 | width: 20px; 137 | z-index: 1337; 138 | } 139 | resize-handle:active, 140 | resize-handle:hover { 141 | opacity: 0.8; 142 | transition: opacity 100ms; 143 | } 144 | -------------------------------------------------------------------------------- /nih_plug_vizia/src/assets.rs: -------------------------------------------------------------------------------- 1 | //! Binary assets for use with `nih_plug_vizia`. These fonts first need to be registered using their 2 | //! associated registration function. 3 | 4 | use vizia::prelude::*; 5 | 6 | // This module provides a re-export and simple font wrappers around the re-exported fonts. 7 | pub use nih_plug_assets::*; 8 | 9 | /// The font name for the Noto Sans font family. Comes in regular, thin, light and bold versions, 10 | /// with italic variations for each. Register the variations you want to use with 11 | /// [`register_noto_sans_regular()`], [`register_noto_sans_regular_italic()`], 12 | /// [`register_noto_sans_thin()`], [`register_noto_sans_thin_italic()`], 13 | /// [`register_noto_sans_light()`], [`register_noto_sans_light_italic()`], 14 | /// [`register_noto_sans_bold()`], and [`register_noto_sans_bold_italic()`], Use the font weight and 15 | /// font style properties to select a specific variation. 16 | pub const NOTO_SANS: &str = "Noto Sans"; 17 | 18 | pub fn register_noto_sans_regular(cx: &mut Context) { 19 | cx.add_font_mem(fonts::NOTO_SANS_REGULAR); 20 | } 21 | pub fn register_noto_sans_regular_italic(cx: &mut Context) { 22 | cx.add_font_mem(fonts::NOTO_SANS_REGULAR_ITALIC); 23 | } 24 | pub fn register_noto_sans_thin(cx: &mut Context) { 25 | cx.add_font_mem(fonts::NOTO_SANS_THIN); 26 | } 27 | pub fn register_noto_sans_thin_italic(cx: &mut Context) { 28 | cx.add_font_mem(fonts::NOTO_SANS_THIN_ITALIC); 29 | } 30 | pub fn register_noto_sans_light(cx: &mut Context) { 31 | cx.add_font_mem(fonts::NOTO_SANS_LIGHT); 32 | } 33 | pub fn register_noto_sans_light_italic(cx: &mut Context) { 34 | cx.add_font_mem(fonts::NOTO_SANS_LIGHT_ITALIC); 35 | } 36 | pub fn register_noto_sans_bold(cx: &mut Context) { 37 | cx.add_font_mem(fonts::NOTO_SANS_BOLD); 38 | } 39 | pub fn register_noto_sans_bold_italic(cx: &mut Context) { 40 | cx.add_font_mem(fonts::NOTO_SANS_BOLD_ITALIC); 41 | } 42 | -------------------------------------------------------------------------------- /nih_plug_vizia/src/vizia_assets.rs: -------------------------------------------------------------------------------- 1 | //! Registration functions for Vizia's built-in fonts. These are not enabled by default in 2 | //! `nih_plug_vizia` to save on binary size. 3 | 4 | use vizia::prelude::*; 5 | 6 | // This module provides a re-export and simple font wrappers around the re-exported fonts. 7 | pub use vizia::fonts; 8 | 9 | /// The font name for the Roboto font family. Comes in regular, bold, and italic variations. 10 | /// Register the variations you want to use with [`register_roboto()`], [`register_roboto_bold()`], 11 | /// and [`register_roboto_italic()`] first. Use the font weight and font style properties to select 12 | /// a specific variation. 13 | pub const ROBOTO: &str = "Roboto"; 14 | /// The font name for the icon font (tabler-icons), needs to be registered using 15 | /// [`register_tabler_icons()`] first. 16 | pub const TABLER_ICONS: &str = "tabler-icons"; 17 | 18 | pub fn register_roboto(cx: &mut Context) { 19 | cx.add_font_mem(fonts::ROBOTO_REGULAR); 20 | } 21 | pub fn register_roboto_bold(cx: &mut Context) { 22 | cx.add_font_mem(fonts::ROBOTO_BOLD); 23 | } 24 | pub fn register_roboto_italic(cx: &mut Context) { 25 | cx.add_font_mem(fonts::ROBOTO_ITALIC); 26 | } 27 | pub fn register_tabler_icons(cx: &mut Context) { 28 | cx.add_font_mem(fonts::TABLER_ICONS); 29 | } 30 | -------------------------------------------------------------------------------- /nih_plug_vizia/src/widgets/generic_ui.rs: -------------------------------------------------------------------------------- 1 | //! Generic UIs for NIH-plug using VIZIA. 2 | 3 | use nih_plug::prelude::{ParamFlags, ParamPtr, Params}; 4 | use vizia::prelude::*; 5 | 6 | use super::{ParamSlider, ParamSliderExt, ParamSliderStyle}; 7 | 8 | /// Shows a generic UI for a [`Params`] object. For additional flexibility you can either use the 9 | /// [`new()`][`Self::new()`] method to have the generic UI decide which widget to use for your 10 | /// parameters, or you can use the [`new_custom()`][`Self::new_custom()`] method to determine this 11 | /// yourself. 12 | pub struct GenericUi; 13 | 14 | impl GenericUi { 15 | /// Creates a new [`GenericUi`] for all provided parameters. Use 16 | /// [`new_custom()`][Self::new_custom()] to decide which widget gets used for each parameter. 17 | /// 18 | /// Wrap this in a [`ScrollView`] for plugins with longer parameter lists: 19 | /// 20 | /// ```ignore 21 | /// ScrollView::new(cx, 0.0, 0.0, false, true, |cx| { 22 | /// GenericUi::new(cx, Data::params); 23 | /// }) 24 | /// .width(Percentage(100.0)); 25 | ///``` 26 | pub fn new(cx: &mut Context, params: L) -> Handle<'_, GenericUi> 27 | where 28 | L: Lens + Clone, 29 | PsRef: AsRef + 'static, 30 | Ps: Params + 'static, 31 | { 32 | // Basic styling is done in the `theme.css` style sheet 33 | Self::new_custom(cx, params, move |cx, param_ptr| { 34 | HStack::new(cx, |cx| { 35 | // Align this on the right 36 | Label::new(cx, unsafe { param_ptr.name() }).class("label"); 37 | 38 | Self::draw_widget(cx, params, param_ptr); 39 | }) 40 | .class("row"); 41 | }) 42 | } 43 | 44 | /// Creates a new [`GenericUi`] for all provided parameters using a custom closure that receives 45 | /// a function that should draw some widget for each parameter. 46 | pub fn new_custom( 47 | cx: &mut Context, 48 | params: L, 49 | mut make_widget: impl FnMut(&mut Context, ParamPtr), 50 | ) -> Handle 51 | where 52 | L: Lens, 53 | PsRef: AsRef + 'static, 54 | Ps: Params + 'static, 55 | { 56 | // Basic styling is done in the `theme.css` style sheet 57 | Self.build(cx, |cx| { 58 | // Rust does not have existential types, otherwise we could have passed functions that 59 | // map `params` to some `impl Param` and everything would have been a lot neater 60 | let param_map = params.map(|params| params.as_ref().param_map()).get(cx); 61 | for (_, param_ptr, _) in param_map { 62 | let flags = unsafe { param_ptr.flags() }; 63 | if flags.contains(ParamFlags::HIDE_IN_GENERIC_UI) { 64 | continue; 65 | } 66 | 67 | make_widget(cx, param_ptr); 68 | } 69 | }) 70 | } 71 | 72 | /// The standard widget drawing function. This can be used together with `.new_custom()` to only 73 | /// draw the labels differently. 74 | pub fn draw_widget(cx: &mut Context, params: L, param_ptr: ParamPtr) 75 | where 76 | L: Lens, 77 | PsRef: AsRef + 'static, 78 | Ps: Params + 'static, 79 | { 80 | unsafe { 81 | match param_ptr { 82 | ParamPtr::FloatParam(p) => ParamSlider::new(cx, params, move |_| &*p), 83 | ParamPtr::IntParam(p) => ParamSlider::new(cx, params, move |_| &*p), 84 | ParamPtr::BoolParam(p) => ParamSlider::new(cx, params, move |_| &*p), 85 | ParamPtr::EnumParam(p) => ParamSlider::new(cx, params, move |_| &*p), 86 | } 87 | } 88 | .set_style(match unsafe { param_ptr.step_count() } { 89 | // This looks nice for boolean values, but it's too crowded for anything beyond 90 | // that without making the widget wider 91 | Some(step_count) if step_count <= 1 => { 92 | ParamSliderStyle::CurrentStepLabeled { even: true } 93 | } 94 | Some(step_count) if step_count <= 2 => ParamSliderStyle::CurrentStep { even: true }, 95 | Some(_) => ParamSliderStyle::FromLeft, 96 | // This is already the default, but continuous parameters should be drawn from 97 | // the center if the default is also centered, or from the left if it is not 98 | None => ParamSliderStyle::Centered, 99 | }) 100 | .class("widget"); 101 | } 102 | } 103 | 104 | impl View for GenericUi { 105 | fn element(&self) -> Option<&'static str> { 106 | Some("generic-ui") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /nih_plug_vizia/src/widgets/util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for writing VIZIA widgets. 2 | 3 | use vizia::prelude::*; 4 | 5 | /// An extension trait for [`Modifiers`] that adds platform-independent getters. 6 | pub trait ModifiersExt { 7 | /// Returns true if the Command (on macOS) or Ctrl (on any other platform) key is pressed. 8 | fn command(&self) -> bool; 9 | 10 | /// Returns true if the Alt (or Option on macOS) key is pressed. 11 | fn alt(&self) -> bool; 12 | 13 | /// Returns true if the Shift key is pressed. 14 | fn shift(&self) -> bool; 15 | } 16 | 17 | impl ModifiersExt for Modifiers { 18 | fn command(&self) -> bool { 19 | #[cfg(target_os = "macos")] 20 | let result = self.contains(Modifiers::LOGO); 21 | 22 | #[cfg(not(target_os = "macos"))] 23 | let result = self.contains(Modifiers::CTRL); 24 | 25 | result 26 | } 27 | 28 | fn alt(&self) -> bool { 29 | self.contains(Modifiers::ALT) 30 | } 31 | 32 | fn shift(&self) -> bool { 33 | self.contains(Modifiers::SHIFT) 34 | } 35 | } 36 | 37 | /// Remap a `[0, 1]` value to an x-coordinate within the current entity's bounding box. The value 38 | /// will be clamped to `[0, 1]` if it isn't already in that range. This ignores the border width. 39 | pub fn remap_current_entity_x_t(cx: &EventContext, t: f32) -> f32 { 40 | let border_width = cx.border_width(); 41 | let x_pos = cx.cache.get_posx(cx.current()) + border_width; 42 | let width = cx.cache.get_width(cx.current()) - (border_width * 2.0); 43 | x_pos + (width * t.clamp(0.0, 1.0)) 44 | } 45 | 46 | /// Remap a `[0, 1]` value to a y-coordinate within the current entity's bounding box. The value 47 | /// will be clamped to `[0, 1]` if it isn't already in that range. This ignores the border width. 48 | /// 49 | /// # Note 50 | /// 51 | /// As y-coordinates increase from top to bottom, this value is likely the inverse of what you want. 52 | /// 0.0 is the bottom of the enitty and 1.0 corresponds to the top of the entity. 53 | pub fn remap_current_entity_y_t(cx: &EventContext, t: f32) -> f32 { 54 | let border_width = cx.border_width(); 55 | let y_pos = cx.cache.get_posy(cx.current()) + border_width; 56 | let height = cx.cache.get_height(cx.current()) - (border_width * 2.0); 57 | y_pos + (height * t.clamp(0.0, 1.0)) 58 | } 59 | 60 | /// Remap an x-coordinate to a `[0, 1]` value within the current entity's bounding box. The value 61 | /// will be clamped to `[0, 1]` if it isn't already in that range. This ignores the border width. 62 | pub fn remap_current_entity_x_coordinate(cx: &EventContext, x_coord: f32) -> f32 { 63 | let border_width = cx.border_width(); 64 | let x_pos = cx.cache.get_posx(cx.current()) + border_width; 65 | let width = cx.cache.get_width(cx.current()) - (border_width * 2.0); 66 | ((x_coord - x_pos) / width).clamp(0.0, 1.0) 67 | } 68 | 69 | /// Remap an y-coordinate to a `[0, 1]` value within the current entity's bounding box. The value 70 | /// will be clamped to `[0, 1]` if it isn't already in that range. This ignores the border width. 71 | /// 72 | /// # Note 73 | /// 74 | /// As y-coordinates increase from top to bottom, this value is likely the inverse of what you want. 75 | /// 0.0 is the bottom of the enitty and 1.0 corresponds to the top of the entity. 76 | pub fn remap_current_entity_y_coordinate(cx: &EventContext, y_coord: f32) -> f32 { 77 | let border_width = cx.border_width(); 78 | let y_pos = cx.cache.get_posy(cx.current()) + border_width; 79 | let height = cx.cache.get_height(cx.current()) - (border_width * 2.0); 80 | ((y_coord - y_pos) / height).clamp(0.0, 1.0) 81 | } 82 | -------------------------------------------------------------------------------- /nih_plug_xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nih_plug_xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | description = "NIH-plug's cargo xtask command, as a library" 7 | license = "ISC" 8 | 9 | [dependencies] 10 | anyhow = "1.0" 11 | cargo_metadata = "0.18.1" 12 | goblin = "0.6.1" 13 | # Version 0.1.3 from crates.io assumes a 64-bit toolchain 14 | reflink = { git = "https://github.com/nicokoch/reflink.git", rev = "e8d93b465f5d9ad340cd052b64bbc77b8ee107e2" } 15 | serde = { version = "1.0", features = ["derive"] } 16 | toml = "0.7.2" 17 | -------------------------------------------------------------------------------- /nih_plug_xtask/README.md: -------------------------------------------------------------------------------- 1 | # NIH-plug: bundler and other utilities 2 | 3 | This is NIH-plug's `cargo xtask` command, as a library. This way you can use it 4 | in your own projects without having to either fork this repo or vendor the 5 | binary into your own repo. This is necessary until Cargo supports [running 6 | binaries from dependencies 7 | directly](https://github.com/rust-lang/rfcs/pull/3168). 8 | 9 | To use this, add an `xtask` binary to your project using `cargo new --bin xtask`. Then add that binary to the Cargo workspace in your repository's main 10 | `Cargo.toml` file like so: 11 | 12 | ```toml 13 | # Cargo.toml 14 | 15 | [workspace] 16 | members = ["xtask"] 17 | ``` 18 | 19 | Add `nih_plug_xtask` to the new xtask package's dependencies, and call its main 20 | function from the new xtask binary: 21 | 22 | ```toml 23 | # xtask/Cargo.toml 24 | 25 | [dependencies] 26 | nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug.git" } 27 | ``` 28 | 29 | ```rust 30 | // xtask/src/main.rs 31 | 32 | fn main() -> nih_plug_xtask::Result<()> { 33 | nih_plug_xtask::main() 34 | } 35 | ``` 36 | 37 | Lastly, create a `.cargo/config` file in your repository and add a Cargo alias. 38 | This allows you to run the binary using `cargo xtask`: 39 | 40 | ```toml 41 | # .cargo/config 42 | 43 | [alias] 44 | xtask = "run --package xtask --release --" 45 | ``` 46 | -------------------------------------------------------------------------------- /nih_plug_xtask/src/symbols.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | /// Check whether a binary exports the specified symbol. Used to detect the plugin formats supported 6 | /// by a plugin library. Returns an error if the binary cuuld not be read. This function will also 7 | /// parse non-native binaries. 8 | pub fn exported>(binary: P, symbol: &str) -> Result { 9 | // Parsing the raw binary instead of relying on nm-like tools makes cross compiling a bit easier 10 | let bytes = fs::read(&binary) 11 | .with_context(|| format!("Could not read '{}'", binary.as_ref().display()))?; 12 | match goblin::Object::parse(&bytes)? { 13 | goblin::Object::Elf(obj) => Ok(obj 14 | .dynsyms 15 | .iter() 16 | // We don't filter by functions here since we need to export a constant for CLAP 17 | .any(|sym| !sym.is_import() && obj.dynstrtab.get_at(sym.st_name) == Some(symbol))), 18 | goblin::Object::Mach(obj) => { 19 | let obj = match obj { 20 | goblin::mach::Mach::Fat(arches) => match arches 21 | .get(0) 22 | .context("Fat Mach-O binary without any binaries")? 23 | { 24 | goblin::mach::SingleArch::MachO(obj) => obj, 25 | // THis shouldn't be hit 26 | goblin::mach::SingleArch::Archive(_) => { 27 | anyhow::bail!( 28 | "'{}' contained an unexpected Mach-O archive", 29 | binary.as_ref().display() 30 | ) 31 | } 32 | }, 33 | goblin::mach::Mach::Binary(obj) => obj, 34 | }; 35 | 36 | // XXX: Why are all exported symbols on macOS prefixed with an underscore? 37 | let symbol = format!("_{symbol}"); 38 | 39 | Ok(obj.exports()?.into_iter().any(|sym| sym.name == symbol)) 40 | } 41 | goblin::Object::PE(obj) => Ok(obj.exports.iter().any(|sym| sym.name == Some(symbol))), 42 | obj => bail!("Unsupported object type: {:?}", obj), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /nih_plug_xtask/src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::fs; 3 | use std::path::Path; 4 | use std::process::Command; 5 | 6 | use crate::CompilationTarget; 7 | 8 | /// Acts the same as [`reflink::reflink_or_copy()`], but it removes existing files first. This works 9 | /// around a limitation of macOS that the reflink crate also applies to other platforms to stay 10 | /// consistent. See the [`reflink`] crate documentation or #26 for more information. 11 | pub fn reflink, Q: AsRef>(from: P, to: Q) -> Result> { 12 | let to = to.as_ref(); 13 | if to.exists() { 14 | fs::remove_file(to).context("Could not remove file before reflinking")?; 15 | } 16 | 17 | reflink::reflink_or_copy(from, to).context("Could not reflink or copy file") 18 | } 19 | 20 | /// Either reflink `from` to `to` if `from` contains a single element, or combine multiple binaries 21 | /// into `to` depending on the compilation target 22 | pub fn reflink_or_combine>( 23 | from: &[&Path], 24 | to: P, 25 | compilation_target: CompilationTarget, 26 | ) -> Result<()> { 27 | match (from, compilation_target) { 28 | ([], _) => anyhow::bail!("The 'from' slice is empty"), 29 | ([path], _) => { 30 | reflink(path, to.as_ref()).with_context(|| { 31 | format!( 32 | "Could not copy {} to {}", 33 | path.display(), 34 | to.as_ref().display() 35 | ) 36 | })?; 37 | } 38 | (paths, CompilationTarget::MacOSUniversal) => { 39 | lipo(paths, to.as_ref()) 40 | .with_context(|| format!("Could not create universal binary from {paths:?}"))?; 41 | } 42 | _ => anyhow::bail!( 43 | "Combining multiple binaries is not yet supported for {compilation_target:?}." 44 | ), 45 | }; 46 | 47 | Ok(()) 48 | } 49 | 50 | /// Combine multiple macOS binaries into a universal macOS binary. 51 | pub fn lipo(inputs: &[&Path], target: &Path) -> Result<()> { 52 | let status = Command::new("lipo") 53 | .arg("-create") 54 | .arg("-output") 55 | .arg(target) 56 | .args(inputs) 57 | .status() 58 | .context("Could not call the 'lipo' binary to create a universal macOS binary")?; 59 | if !status.success() { 60 | anyhow::bail!( 61 | "Could not call the 'lipo' binary to create a universal macOS binary from {inputs:?}", 62 | ); 63 | } else { 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /plugins/buffr_glitch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic 7 | Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [0.2.0] - 2023-01-17 10 | 11 | ### Added 12 | 13 | - Added polyphony support. You can now play up to eight voices at once. 14 | - Added an option to crossfade the recorded buffer to avoid or dial down clicks 15 | when looping. 16 | - Added attack and release time controls to avoid or dial down clicks when 17 | pressing or releasing a key. 18 | - Added an optional velocity sensitive mode. 19 | - Added support for polyphonic CLAP and VST3 volume note expressions to 20 | precisely control each note's volume while it's playing. 21 | 22 | ### Removed 23 | 24 | - The normalization option been removed since the old method to automatically 25 | normalize the buffer doesn't work anymore with recording change mentioned 26 | below. 27 | 28 | ### Changed 29 | 30 | - Buffr Glitch now starts recording when a note is held down instead of playing 31 | back previously played audio. This makes it possible to use Buffr Glitch in a 32 | more rhythmic way without manually offsetting notes. This is particularly 33 | important at the start of the playback since then the buffer will have 34 | otherwise been completely silent, and also when playing chords. 35 | -------------------------------------------------------------------------------- /plugins/buffr_glitch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "buffr_glitch" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/buffr_glitch" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | -------------------------------------------------------------------------------- /plugins/buffr_glitch/README.md: -------------------------------------------------------------------------------- 1 | # Buffr Glitch 2 | 3 | Like the sound of a CD player skipping? Well, do I have the plugin for you. This 4 | plugin is essentially a MIDI triggered buffer repeat plugin. When you play a 5 | note, the plugin will sample the period corresponding to that note's frequency 6 | and use that as a single waveform cycle. This can end up sounding like an 7 | in-tune glitch when used sparingly, or like a weird synthesizer when used less 8 | subtly. 9 | 10 | ## Tips 11 | 12 | - You can control the buffer's gain by enabling the velocity sensitive mode and 13 | changing the velocity. In Bitwig Studio and other DAWs that support volume 14 | note expressions you can also control the gain that way. 15 | 16 | ## Download 17 | 18 | You can download the development binaries for Linux, Windows and macOS from the 19 | [automated 20 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 21 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 22 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 23 | 24 | On macOS you may need to [disable 25 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 26 | more difficult to run unsigned code on macOS. 27 | 28 | ### Building 29 | 30 | After installing [Rust](https://rustup.rs/), you can compile Buffr Glitch as 31 | follows: 32 | 33 | ```shell 34 | cargo xtask bundle buffr_glitch --release 35 | ``` 36 | -------------------------------------------------------------------------------- /plugins/buffr_glitch/src/envelope.rs: -------------------------------------------------------------------------------- 1 | // Buffr Glitch: a MIDI-controlled buffer repeater 2 | // Copyright (C) 2022-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use nih_plug::nih_debug_assert; 18 | 19 | /// The most barebones envelope generator you can imagine using a bog standard first order IIR 20 | /// filter. We don't need anything fancy right now. This returns values in the range `[0, 1]`. 21 | #[derive(Debug, Default)] 22 | pub struct AREnvelope { 23 | /// The internal filter state. 24 | state: f32, 25 | 26 | /// For each sample, the output becomes `(state * t) + (target * (1.0 - t))`. This is `t` during 27 | /// the attack portion of the envelope generator. 28 | attack_retain_t: f32, 29 | /// `attack_retain_t`, but for the release portion. 30 | release_retain_t: f32, 31 | 32 | /// Whether the envelope follower is currently in its release stage. 33 | releasing: bool, 34 | } 35 | 36 | impl AREnvelope { 37 | pub fn set_attack_time(&mut self, sample_rate: f32, time_ms: f32) { 38 | self.attack_retain_t = (-1.0 / (time_ms / 1000.0 * sample_rate)).exp(); 39 | } 40 | 41 | pub fn set_release_time(&mut self, sample_rate: f32, time_ms: f32) { 42 | self.release_retain_t = (-1.0 / (time_ms / 1000.0 * sample_rate)).exp(); 43 | } 44 | 45 | /// Completely reset the envelope follower. 46 | pub fn reset(&mut self) { 47 | self.state = 0.0; 48 | self.releasing = false; 49 | } 50 | 51 | /// Return the current/previously returned value. 52 | pub fn current(&self) -> f32 { 53 | self.state 54 | } 55 | 56 | /// Compute the next `block_len` values and store them in `block_values`. 57 | pub fn next_block(&mut self, block_values: &mut [f32], block_len: usize) { 58 | nih_debug_assert!(block_values.len() >= block_len); 59 | for value in block_values.iter_mut().take(block_len) { 60 | let (target, t) = if self.releasing { 61 | (0.0, self.release_retain_t) 62 | } else { 63 | (1.0, self.attack_retain_t) 64 | }; 65 | 66 | let new = (self.state * t) + (target * (1.0 - t)); 67 | self.state = new; 68 | 69 | *value = new; 70 | } 71 | } 72 | 73 | /// Start the release segment of the envelope generator. 74 | pub fn start_release(&mut self) { 75 | self.releasing = true; 76 | } 77 | 78 | /// Whether the envelope generator is still in its release stage and the value hasn't dropped 79 | /// down to 0.0 yet. 80 | pub fn is_releasing(&self) -> bool { 81 | self.releasing && self.state >= 0.001 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugins/crisp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crisp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/crisp" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | nih_plug_vizia = { path = "../../nih_plug_vizia" } 15 | -------------------------------------------------------------------------------- /plugins/crisp/README.md: -------------------------------------------------------------------------------- 1 | # Crisp 2 | 3 | This plugin adds a bright crispy top end to low bass sounds. The effect was 4 | inspired by Polarity's [Fake Distortion](https://youtu.be/MKfFn4L1zeg) video. 5 | 6 | ## Download 7 | 8 | You can download the development binaries for Linux, Windows and macOS from the 9 | [automated 10 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 11 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 12 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 13 | 14 | On macOS you may need to [disable 15 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 16 | more difficult to run unsigned code on macOS. 17 | 18 | ### Building 19 | 20 | After installing [Rust](https://rustup.rs/), you can compile Crisp as follows: 21 | 22 | ```shell 23 | cargo xtask bundle crisp --release 24 | ``` 25 | -------------------------------------------------------------------------------- /plugins/crisp/src/editor.rs: -------------------------------------------------------------------------------- 1 | // Crisp: a distortion plugin but not quite 2 | // Copyright (C) 2022-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use nih_plug::prelude::Editor; 18 | use nih_plug_vizia::vizia::prelude::*; 19 | use nih_plug_vizia::widgets::*; 20 | use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; 21 | use std::sync::Arc; 22 | 23 | use crate::CrispParams; 24 | 25 | #[derive(Lens)] 26 | struct Data { 27 | params: Arc, 28 | } 29 | 30 | impl Model for Data {} 31 | 32 | // Makes sense to also define this here, makes it a bit easier to keep track of 33 | pub(crate) fn default_state() -> Arc { 34 | ViziaState::new(|| (400, 390)) 35 | } 36 | 37 | pub(crate) fn create( 38 | params: Arc, 39 | editor_state: Arc, 40 | ) -> Option> { 41 | create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| { 42 | assets::register_noto_sans_light(cx); 43 | assets::register_noto_sans_thin(cx); 44 | 45 | Data { 46 | params: params.clone(), 47 | } 48 | .build(cx); 49 | 50 | VStack::new(cx, |cx| { 51 | Label::new(cx, "Crisp") 52 | .font_family(vec![FamilyOwned::Name(String::from(assets::NOTO_SANS))]) 53 | .font_weight(FontWeightKeyword::Thin) 54 | .font_size(30.0) 55 | .height(Pixels(50.0)) 56 | .child_top(Stretch(1.0)) 57 | .child_bottom(Pixels(1.0)) 58 | // Make this more or less align with the parameters column 59 | .right(Pixels(67.0)); 60 | 61 | ScrollView::new(cx, 0.0, 0.0, false, true, |cx| { 62 | // This looks better if it's flush at the top, and then we'll just add some padding 63 | // at the top of the scroll view 64 | GenericUi::new(cx, Data::params).child_top(Pixels(0.0)); 65 | }) 66 | .width(Percentage(100.0)) 67 | .top(Pixels(5.0)); 68 | }) 69 | .row_between(Pixels(0.0)) 70 | .child_left(Stretch(1.0)) 71 | .child_right(Stretch(1.0)); 72 | 73 | ResizeHandle::new(cx); 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /plugins/crisp/src/pcg.rs: -------------------------------------------------------------------------------- 1 | // Crisp: a distortion plugin but not quite 2 | // Copyright (C) 2022-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | //! A minimal implementation of `pcg32i` PRNG from the PCG library. Implemented separately instead 18 | //! of using the rand crate implementation so we can adapt this for SIMD use. 19 | //! 20 | //! 21 | //! 22 | 23 | const PCG_DEFAULT_MULTIPLIER_32: u32 = 747796405; 24 | 25 | /// The `pcg32i` PRNG from PCG. 26 | #[derive(Copy, Clone)] 27 | pub struct Pcg32iState { 28 | state: u32, 29 | inc: u32, 30 | } 31 | 32 | impl Pcg32iState { 33 | /// Initialize the PRNG, aka `*_srandom()`. 34 | /// 35 | /// 36 | pub const fn new(state: u32, sequence: u32) -> Self { 37 | let mut rng = Self { 38 | state: 0, 39 | inc: (sequence << 1) | 1, 40 | }; 41 | 42 | // https://github.com/imneme/pcg-c/blob/83252d9c23df9c82ecb42210afed61a7b42402d7/include/pcg_variants.h#L540-L543, 43 | // inlined so we can make this a const function 44 | rng.state = rng 45 | .state 46 | .wrapping_mul(PCG_DEFAULT_MULTIPLIER_32) 47 | .wrapping_add(rng.inc); 48 | rng.state += state; 49 | rng.state = rng 50 | .state 51 | .wrapping_mul(PCG_DEFAULT_MULTIPLIER_32) 52 | .wrapping_add(rng.inc); 53 | 54 | rng 55 | } 56 | 57 | /// Generate a new uniformly distirubted `u32` covering all possible values. 58 | /// 59 | /// 60 | #[inline] 61 | pub fn next_u32(&mut self) -> u32 { 62 | let old_state = self.state; 63 | self.state = self 64 | .state 65 | .wrapping_mul(PCG_DEFAULT_MULTIPLIER_32) 66 | .wrapping_add(self.inc); 67 | 68 | let word = ((old_state >> ((old_state >> 28) + 4)) ^ old_state).wrapping_mul(277803737); 69 | (word >> 22) ^ word 70 | } 71 | 72 | /// Generate a new `f32` value in the open `(0, 1)` range. 73 | #[inline] 74 | pub fn next_f32(&mut self) -> f32 { 75 | const FLOAT_SIZE: u32 = std::mem::size_of::() as u32 * 8; 76 | 77 | // Implementation from https://docs.rs/rand/0.8.4/rand/distributions/struct.Open01.html 78 | let value = self.next_u32(); 79 | let fraction = value >> (FLOAT_SIZE - f32::MANTISSA_DIGITS - 1); 80 | 81 | let exponent_bits: u32 = ((f32::MAX_EXP - 1) as u32) << (f32::MANTISSA_DIGITS - 1); 82 | f32::from_bits(fraction | exponent_bits) - (1.0 - f32::EPSILON / 2.0) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /plugins/crossover/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crossover" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/crossover" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | default = ["simd"] 14 | # Make it go fast, vroom. Requires a nightly compiler. Non-SIMD builds are 15 | # currently unsupported. 16 | simd = ["nih_plug/simd"] 17 | 18 | [dependencies] 19 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 20 | realfft = "3.0.0" 21 | -------------------------------------------------------------------------------- /plugins/crossover/README.md: -------------------------------------------------------------------------------- 1 | # Crossover 2 | 3 | This plugin is as boring as it sounds. It cleanly splits the signal into two to 4 | five bands using a variety of algorithms. Those bands are then sent to auxiliary 5 | outputs so they can be accessed and processed individually. Meant as an 6 | alternative to Bitwig's Multiband FX devices but with cleaner crossovers and a 7 | linear-phase option. 8 | 9 | In Bitwig Studio you'll want to click on the 'Show plug-in multi-out chain 10 | selector' button and then on 'Add missing chains' to access the chains. The main 11 | output will not output any audio. To save time, you can save this setup as the 12 | default preset by right clicking on the device. Any new Crossover instances will 13 | then already have the additional output chains set up. You can also download 14 | [this 15 | preset](https://cdn.discordapp.com/attachments/767397282344599602/1096417371880685669/Crossover_setup.bwpreset), 16 | load it, and then set it as your default preset for Crossover. 17 | 18 | 19 | 20 | ![Screenshot in Bitwig](https://i.imgur.com/hrn9uhR.png) 21 | 22 | ## Download 23 | 24 | You can download the development binaries for Linux, Windows and macOS from the 25 | [automated 26 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 27 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 28 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 29 | 30 | On macOS you may need to [disable 31 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 32 | more difficult to run unsigned code on macOS. 33 | 34 | ### Building 35 | 36 | After installing **nightly** [Rust](https://rustup.rs/) toolchain, you can 37 | compile Crossover as follows: 38 | 39 | ```shell 40 | cargo +nightly xtask bundle crossover --release 41 | ``` 42 | -------------------------------------------------------------------------------- /plugins/crossover/src/crossover.rs: -------------------------------------------------------------------------------- 1 | // Crossover: clean crossovers as a multi-out plugin 2 | // Copyright (C) 2022-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | pub mod fir; 18 | pub mod iir; 19 | -------------------------------------------------------------------------------- /plugins/diopser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic 7 | Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ### Changed 12 | 13 | - On Windows, clicking on the plugin's name no longer takes you to Spectral 14 | Compressor's home page. This is a temporary workaround for an issue with an 15 | underlying library. 16 | - Rendering the GUI now takes slightly less resources. 17 | -------------------------------------------------------------------------------- /plugins/diopser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diopser" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/diopser" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | default = ["simd"] 14 | # Make it go fast, vroom. Requires a nightly compiler. Support for the non-SIMD 15 | # version has been removed at the moment. 16 | simd = ["nih_plug/simd"] 17 | 18 | [dependencies] 19 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 20 | nih_plug_vizia = { path = "../../nih_plug_vizia" } 21 | 22 | atomic_float = "0.1" 23 | semver = "1.0.14" 24 | 25 | # For the GUI 26 | realfft = "3.0" 27 | open = "3.0" 28 | triple_buffer = "6.0" 29 | -------------------------------------------------------------------------------- /plugins/diopser/README.md: -------------------------------------------------------------------------------- 1 | # Diopser 2 | 3 | You were expecting Disperser[¹](#disperser), but it was me, Diopser! 4 | 5 | Diopser lets you rotate the phase of a signal around a specific frequency 6 | without affecting its spectral content. This effect can be used to emphasize 7 | transients and other parts of a sound that in a way that isn't possible with 8 | regular equalizers or dynamics processors, especially when applied to low 9 | pitched or wide band sounds. More extreme settings will make everything sound 10 | like a cartoon laser beam, or a psytrance kickdrum. 11 | 12 | ![Screenshot](https://i.imgur.com/QLtHtQL.png) 13 | 14 | This is a port of https://github.com/robbert-vdh/diopser with more features and 15 | much better performance. 16 | 17 | 18 | *Disperser is a trademark of Kilohearts AB. Diopser is in no way related to 19 | Disperser or Kilohearts AB. 20 | 21 | 22 | ## Tips 23 | 24 | - Alt+click on the spectrum analyzer to enter to enter a frequency value in 25 | Hertz or musical notes. 26 | - Hold down Alt/Option while dragging the filter frequency around to snap to 27 | whole notes. 28 | - The safe mode is enabled by default. This limits the frequency range and the 29 | number of filter stages. Simply disable the safe mode if you want to crank 30 | everything up to 11. With safe mode disabled you may find that going down to 31 | the bottom of the frequency range introduces some loud low frequency 32 | resonances, especially when combined with a lot of filter stages. In that case 33 | you may want to use a peak limiter after this plugin until you understand how 34 | it reacts to different changes. Or maybe you'll want to check out [Safety 35 | Limiter](../safety_limiter), which is made for this exact purpose. 36 | - Turn down the automation precision to reduce the DSP load hit of changing the 37 | filter frequency and resonance at the cost of introducing varying amounts of 38 | aliasing and zipper noises. 39 | - The aforementioned artifacts introduced by setting a low automation precision 40 | can actually be useful for sound design purposes. 41 | - Change the number of filter stages to immediately reset the filters and stop 42 | ringing. 43 | 44 | ## Download 45 | 46 | You can download the development binaries for Linux, Windows and macOS from the 47 | [automated 48 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 49 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 50 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 51 | 52 | On macOS you may need to [disable 53 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 54 | more difficult to run unsigned code on macOS. 55 | 56 | ### Building 57 | 58 | After installing [Rust](https://rustup.rs/) with the nightly toolchain (because 59 | of the use of SIMD), you can compile Diopser as follows: 60 | 61 | ```shell 62 | cargo +nightly xtask bundle diopser --release 63 | ``` 64 | -------------------------------------------------------------------------------- /plugins/diopser/src/editor/analyzer.rs: -------------------------------------------------------------------------------- 1 | // Diopser: a phase rotation plugin 2 | // Copyright (C) 2021-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use atomic_float::AtomicF32; 18 | use nih_plug::nih_debug_assert; 19 | use nih_plug::prelude::FloatRange; 20 | use nih_plug_vizia::vizia::prelude::*; 21 | use nih_plug_vizia::vizia::vg; 22 | use std::sync::atomic::Ordering; 23 | use std::sync::{Arc, Mutex}; 24 | 25 | use crate::params; 26 | use crate::spectrum::SpectrumOutput; 27 | 28 | /// A very abstract spectrum analyzer. This draws the magnitude spectrum's bins as vertical lines 29 | /// with the same distribution as the filter frequency parameter.. 30 | pub struct SpectrumAnalyzer { 31 | spectrum: Arc>, 32 | sample_rate: Arc, 33 | 34 | /// A function that the x-parameter's/frequency parameter's normalized value to a `[0, 1]` value 35 | /// that is used to display the parameter. This range may end up zooming in on a part of the 36 | /// parameter's original range when safe mode is enabled. 37 | x_renormalize_display: Box f32>, 38 | 39 | /// The same range as that used by the filter frequency parameter. We'll use this to make sure 40 | /// we draw the spectrum analyzer's ticks at locations that match the frequency parameter linked 41 | /// to the X-Y pad's X-axis. 42 | frequency_range: FloatRange, 43 | } 44 | 45 | impl SpectrumAnalyzer { 46 | /// Creates a new [`SpectrumAnalyzer`]. The uses custom drawing. 47 | pub fn new( 48 | cx: &mut Context, 49 | spectrum: LSpectrum, 50 | sample_rate: LRate, 51 | x_renormalize_display: impl Fn(f32) -> f32 + Clone + 'static, 52 | ) -> Handle 53 | where 54 | LSpectrum: Lens>>, 55 | LRate: Lens>, 56 | { 57 | Self { 58 | spectrum: spectrum.get(cx), 59 | sample_rate: sample_rate.get(cx), 60 | 61 | frequency_range: params::filter_frequency_range(), 62 | x_renormalize_display: Box::new(x_renormalize_display), 63 | } 64 | .build( 65 | cx, 66 | // This is an otherwise empty element only used for custom drawing 67 | |_cx| (), 68 | ) 69 | } 70 | } 71 | 72 | impl View for SpectrumAnalyzer { 73 | fn element(&self) -> Option<&'static str> { 74 | Some("spectrum-analyzer") 75 | } 76 | 77 | fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { 78 | let bounds = cx.bounds(); 79 | if bounds.w == 0.0 || bounds.h == 0.0 { 80 | return; 81 | } 82 | 83 | // This spectrum buffer is written to at the end of the process function when the editor is 84 | // open 85 | let mut spectrum = self.spectrum.lock().unwrap(); 86 | let spectrum = spectrum.read(); 87 | let nyquist = self.sample_rate.load(Ordering::Relaxed) / 2.0; 88 | 89 | // This skips background and border drawing 90 | // NOTE: We could do the same thing like in Spectral Compressor and draw part of this 91 | // spectrum analyzer as a single mesh but for whatever erason the aliasing/moire 92 | // pattern here doesn't look nearly as bad. 93 | let line_width = cx.scale_factor() * 1.5; 94 | let paint = vg::Paint::color(cx.font_color().into()).with_line_width(line_width); 95 | let mut path = vg::Path::new(); 96 | for (bin_idx, magnitude) in spectrum.iter().enumerate() { 97 | // We'll match up the bin's x-coordinate with the filter frequency parameter 98 | let frequency = (bin_idx as f32 / spectrum.len() as f32) * nyquist; 99 | // NOTE: This takes the safe-mode switch into acocunt. When it is enabled, the range is 100 | // zoomed in to match the X-Y pad. 101 | let t = (self.x_renormalize_display)(self.frequency_range.normalize(frequency)); 102 | if t <= 0.0 || t >= 1.0 { 103 | continue; 104 | } 105 | 106 | // Scale this so that 1.0/0 dBFS magnitude is at 80% of the height, the bars begin at 107 | // -80 dBFS, and that the scaling is linear 108 | nih_debug_assert!(*magnitude >= 0.0); 109 | let magnitude_db = nih_plug::util::gain_to_db(*magnitude); 110 | let height = ((magnitude_db + 80.0) / 100.0).clamp(0.0, 1.0); 111 | 112 | path.move_to( 113 | bounds.x + (bounds.w * t), 114 | bounds.y + (bounds.h * (1.0 - height)), 115 | ); 116 | path.line_to(bounds.x + (bounds.w * t), bounds.y + bounds.h); 117 | } 118 | 119 | canvas.stroke_path(&path, &paint); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /plugins/diopser/src/editor/button.rs: -------------------------------------------------------------------------------- 1 | // Diopser: a phase rotation plugin 2 | // Copyright (C) 2021-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use nih_plug_vizia::vizia::prelude::*; 18 | 19 | use super::SafeModeClamper; 20 | 21 | /// A custom toggleable button that toggles safe mode whenever it is Alt+clicked. Otherwise this is 22 | /// very similar to the param button. 23 | #[derive(Lens)] 24 | pub struct SafeModeButton> { 25 | lens: L, 26 | 27 | /// The number of (fractional) scrolled lines that have not yet been turned into parameter 28 | /// change events. This is needed to support trackpads with smooth scrolling. 29 | scrolled_lines: f32, 30 | } 31 | 32 | impl> SafeModeButton { 33 | /// Creates a new button bound to the [`SafeModeClamper`]. 34 | pub fn new(cx: &mut Context, lens: L, label: impl Res + Clone) -> Handle 35 | where 36 | T: ToString, 37 | { 38 | Self { 39 | lens, 40 | scrolled_lines: 0.0, 41 | } 42 | .build(cx, |cx| { 43 | Label::new(cx, label).hoverable(false); 44 | }) 45 | .checked(lens.map(|v| v.status())) 46 | // We'll pretend this is a param-button, so this class is used for assigning a unique color 47 | .class("safe-mode") 48 | } 49 | } 50 | 51 | impl> View for SafeModeButton { 52 | fn element(&self) -> Option<&'static str> { 53 | // Reuse the styling from param-button 54 | Some("param-button") 55 | } 56 | 57 | fn event(&mut self, cx: &mut EventContext, event: &mut Event) { 58 | event.map(|window_event, meta| match window_event { 59 | // We don't need special double and triple click handling 60 | WindowEvent::MouseDown(MouseButton::Left) 61 | | WindowEvent::MouseDoubleClick(MouseButton::Left) 62 | | WindowEvent::MouseTripleClick(MouseButton::Left) => { 63 | // We can just unconditionally toggle the boolean here. When safe mode is enabled 64 | // this immediately clamps the affected parameters to their new range. 65 | let safe_mode_clamper = self.lens.get(cx); 66 | safe_mode_clamper.toggle(cx); 67 | 68 | meta.consume(); 69 | } 70 | WindowEvent::MouseScroll(_scroll_x, scroll_y) => { 71 | self.scrolled_lines += scroll_y; 72 | 73 | if self.scrolled_lines.abs() >= 1.0 { 74 | let safe_mode_clamper = self.lens.get(cx); 75 | 76 | if self.scrolled_lines >= 1.0 { 77 | safe_mode_clamper.enable(cx); 78 | self.scrolled_lines -= 1.0; 79 | } else { 80 | safe_mode_clamper.disable(); 81 | self.scrolled_lines += 1.0; 82 | } 83 | } 84 | 85 | meta.consume(); 86 | } 87 | _ => {} 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /plugins/diopser/src/editor/theme.css: -------------------------------------------------------------------------------- 1 | #top-bar { 2 | background-color: #e5e5e5; 3 | height: 50px; 4 | width: 100%; 5 | } 6 | 7 | #param-sliders .param-label { 8 | width: 140px; 9 | height: 30px; 10 | child-top: 1s; 11 | child-right: 10px; 12 | child-bottom: 1s; 13 | child-left: 1s; 14 | } 15 | 16 | /* The safe mode button reuses the param-button element's style with a different class */ 17 | param-button.safe-mode { 18 | background-color: #fff28000; 19 | transition: background-color 100ms; 20 | } 21 | param-button.safe-mode:hover { 22 | background-color: #fff28020; 23 | transition: background-color 100ms; 24 | } 25 | param-button.safe-mode:checked { 26 | background-color: #fff280; 27 | transition: background-color 100ms; 28 | } 29 | 30 | #automation-precision { 31 | /* Can't use width: auto here, but we'll try to roughly match the padding of the top bar buttons */ 32 | width: 170px; 33 | } 34 | 35 | spectrum-analyzer { 36 | color: #0a0a0a; 37 | } 38 | 39 | xy-pad { 40 | overflow: hidden; 41 | } 42 | 43 | xy-pad .xy-pad__tooltip { 44 | opacity: 0; 45 | transition: opacity 100ms; 46 | 47 | background-color: #e5e5e5; 48 | border-color: #0a0a0a; 49 | border-width: 1px; 50 | /* This mimics the text box border radius, everything else is squared off so this adds a bit of flair */ 51 | border-radius: 3px; 52 | width: auto; 53 | height: auto; 54 | child-right: 5px; 55 | child-left: 5px; 56 | } 57 | 58 | xy-pad:hover .xy-pad__tooltip { 59 | opacity: 1; 60 | transition: opacity 100ms; 61 | } 62 | 63 | /* This is a textbox, we want it to somewhat resemble the tooltip */ 64 | xy-pad .xy-pad__value-entry { 65 | background-color: #e5e5e5; 66 | border-color: #0a0a0a; 67 | border-width: 1px; 68 | child-right: 5px; 69 | child-left: 5px; 70 | } 71 | xy-pad .xy-pad__value-entry .textbox_container { 72 | overflow: visible; 73 | text-wrap: false; 74 | width: auto; 75 | } 76 | xy-pad .xy-pad__value-entry .caret { 77 | background-color: #0a0a0a; 78 | } 79 | xy-pad .xy-pad__value-entry .selection { 80 | background-color: #0a0a0a30; 81 | } 82 | 83 | xy-pad__handle { 84 | background-color: #e5e5e5; 85 | border-color: #0a0a0a; 86 | border-radius: 50%; 87 | border-width: 1px; 88 | height: 20px; 89 | translate: -50% -50%; 90 | width: 20px; 91 | } 92 | .xy-pad__handle--modulated { 93 | background-color: #a4eafc69; 94 | border-color: #a4eafc96; 95 | } 96 | -------------------------------------------------------------------------------- /plugins/diopser/src/filter.rs: -------------------------------------------------------------------------------- 1 | // Diopser: a phase rotation plugin 2 | // Copyright (C) 2021-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use nih_plug::debug::nih_debug_assert; 18 | use std::f32::consts; 19 | use std::ops::{Add, Mul, Sub}; 20 | use std::simd::f32x2; 21 | 22 | /// A simple biquad filter with functions for generating coefficients for an all-pass filter. 23 | /// 24 | /// Based on . 25 | /// 26 | /// The type parameter T should be either an `f32` or a SIMD type. 27 | #[derive(Clone, Copy, Debug)] 28 | pub struct Biquad { 29 | pub coefficients: BiquadCoefficients, 30 | s1: T, 31 | s2: T, 32 | } 33 | 34 | /// The coefficients `[b0, b1, b2, a1, a2]` for [`Biquad`]. These coefficients are all 35 | /// prenormalized, i.e. they have been divided by `a0`. 36 | /// 37 | /// The type parameter T should be either an `f32` or a SIMD type. 38 | #[derive(Clone, Copy, Debug)] 39 | pub struct BiquadCoefficients { 40 | b0: T, 41 | b1: T, 42 | b2: T, 43 | a1: T, 44 | a2: T, 45 | } 46 | 47 | /// Either an `f32` or some SIMD vector type of `f32`s that can be used with our biquads. 48 | pub trait SimdType: 49 | Mul + Sub + Add + Copy + Sized 50 | { 51 | fn from_f32(value: f32) -> Self; 52 | } 53 | 54 | impl Default for Biquad { 55 | /// Before setting constants the filter should just act as an identity function. 56 | fn default() -> Self { 57 | Self { 58 | coefficients: BiquadCoefficients::identity(), 59 | s1: T::from_f32(0.0), 60 | s2: T::from_f32(0.0), 61 | } 62 | } 63 | } 64 | 65 | impl Biquad { 66 | /// Process a single sample. 67 | pub fn process(&mut self, sample: T) -> T { 68 | let result = self.coefficients.b0 * sample + self.s1; 69 | 70 | self.s1 = self.coefficients.b1 * sample - self.coefficients.a1 * result + self.s2; 71 | self.s2 = self.coefficients.b2 * sample - self.coefficients.a2 * result; 72 | 73 | result 74 | } 75 | 76 | /// Reset the state to zero, useful after making making large, non-interpolatable changes to the 77 | /// filter coefficients. 78 | pub fn reset(&mut self) { 79 | self.s1 = T::from_f32(0.0); 80 | self.s2 = T::from_f32(0.0); 81 | } 82 | } 83 | 84 | impl BiquadCoefficients { 85 | /// Convert scalar coefficients into the correct vector type. 86 | pub fn from_f32s(scalar: BiquadCoefficients) -> Self { 87 | Self { 88 | b0: T::from_f32(scalar.b0), 89 | b1: T::from_f32(scalar.b1), 90 | b2: T::from_f32(scalar.b2), 91 | a1: T::from_f32(scalar.a1), 92 | a2: T::from_f32(scalar.a2), 93 | } 94 | } 95 | 96 | /// Filter coefficients that would cause the sound to be passed through as is. 97 | pub fn identity() -> Self { 98 | Self::from_f32s(BiquadCoefficients { 99 | b0: 1.0, 100 | b1: 0.0, 101 | b2: 0.0, 102 | a1: 0.0, 103 | a2: 0.0, 104 | }) 105 | } 106 | 107 | /// Compute the coefficients for an all-pass filter. 108 | /// 109 | /// Based on . 110 | pub fn allpass(sample_rate: f32, frequency: f32, q: f32) -> Self { 111 | nih_debug_assert!(sample_rate > 0.0); 112 | nih_debug_assert!(frequency > 0.0); 113 | nih_debug_assert!(frequency < sample_rate / 2.0); 114 | nih_debug_assert!(q > 0.0); 115 | 116 | let omega0 = consts::TAU * (frequency / sample_rate); 117 | let cos_omega0 = omega0.cos(); 118 | let alpha = omega0.sin() / (2.0 * q); 119 | 120 | // We'll prenormalize everything with a0 121 | let a0 = 1.0 + alpha; 122 | let b0 = (1.0 - alpha) / a0; 123 | let b1 = (-2.0 * cos_omega0) / a0; 124 | let b2 = (1.0 + alpha) / a0; 125 | let a1 = (-2.0 * cos_omega0) / a0; 126 | let a2 = (1.0 - alpha) / a0; 127 | 128 | Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) 129 | } 130 | } 131 | 132 | impl SimdType for f32 { 133 | #[inline(always)] 134 | fn from_f32(value: f32) -> Self { 135 | value 136 | } 137 | } 138 | 139 | impl SimdType for f32x2 { 140 | #[inline(always)] 141 | fn from_f32(value: f32) -> Self { 142 | f32x2::splat(value) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /plugins/examples/byo_gui_gl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byo_gui_gl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Billy Messenger <60663878+BillyDM@users.noreply.github.com>"] 6 | license = "ISC" 7 | 8 | description = "A simple example plugin with a raw OpenGL context for rendering" 9 | 10 | [lib] 11 | # The `lib` artifact is needed for the standalone target 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | nih_plug = { path = "../../../", features = ["assert_process_allocs", "standalone"] } 16 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "9a0b42c09d712777b2edb4c5e0cb6baf21e988f0", features = ["opengl"] } 17 | raw-window-handle = "0.5" 18 | glow = "0.16" 19 | crossbeam = "0.8" 20 | atomic_float = "0.1" 21 | # To make the state persistable 22 | serde = { version = "1.0", features = ["derive"] } -------------------------------------------------------------------------------- /plugins/examples/byo_gui_gl/src/main.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | 3 | use byo_gui_gl::MyPlugin; 4 | 5 | fn main() { 6 | nih_export_standalone::(); 7 | } 8 | -------------------------------------------------------------------------------- /plugins/examples/byo_gui_softbuffer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byo_gui_softbuffer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Billy Messenger <60663878+BillyDM@users.noreply.github.com>"] 6 | license = "ISC" 7 | 8 | description = "A simple example plugin with a raw Softbuffer context for rendering" 9 | 10 | [lib] 11 | # The `lib` artifact is needed for the standalone target 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | nih_plug = { path = "../../../", features = ["assert_process_allocs", "standalone"] } 16 | # NOTE: OpenGL support is not needed here, but rust-analyzer gets confused when 17 | # some crates do use it and others don't 18 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "9a0b42c09d712777b2edb4c5e0cb6baf21e988f0", features = ["opengl"]} 19 | softbuffer = { version = "0.4.6", default-features = false, features = ["kms", "x11"]} 20 | raw-window-handle = "0.5" 21 | raw-window-handle-06 = { package = "raw-window-handle", version = "0.6" } 22 | crossbeam = "0.8" 23 | atomic_float = "0.1" 24 | # To make the state persistable 25 | serde = { version = "1.0", features = ["derive"] } -------------------------------------------------------------------------------- /plugins/examples/byo_gui_softbuffer/src/main.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | 3 | use byo_gui_softbuffer::MyPlugin; 4 | 5 | fn main() { 6 | nih_export_standalone::(); 7 | } 8 | -------------------------------------------------------------------------------- /plugins/examples/byo_gui_wgpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "byo_gui_wgpu" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Billy Messenger <60663878+BillyDM@users.noreply.github.com>"] 6 | license = "ISC" 7 | 8 | description = "A simple example plugin with a raw WGPU context for rendering" 9 | 10 | [lib] 11 | # The `lib` artifact is needed for the standalone target 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | nih_plug = { path = "../../../", features = ["assert_process_allocs", "standalone"] } 16 | # NOTE: OpenGL support is not needed here, but rust-analyzer gets confused when 17 | # some crates do use it and others don't 18 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "9a0b42c09d712777b2edb4c5e0cb6baf21e988f0", features = ["opengl"]} 19 | wgpu = "25" 20 | raw-window-handle = "0.5" 21 | raw-window-handle-06 = { package = "raw-window-handle", version = "0.6" } 22 | pollster = "0.4.0" 23 | crossbeam = "0.8" 24 | atomic_float = "0.1" 25 | # To make the state persistable 26 | serde = { version = "1.0", features = ["derive"] } -------------------------------------------------------------------------------- /plugins/examples/byo_gui_wgpu/src/main.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | 3 | use byo_gui_wgpu::MyPlugin; 4 | 5 | fn main() { 6 | nih_export_standalone::(); 7 | } 8 | -------------------------------------------------------------------------------- /plugins/examples/gain/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | 14 | parking_lot = "0.12" 15 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_egui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain_gui_egui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "A simple gain plugin with an egui GUI" 9 | 10 | [lib] 11 | # The `lib` artifact is needed for the standalone target 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | nih_plug = { path = "../../../", features = ["assert_process_allocs", "standalone"] } 16 | nih_plug_egui = { path = "../../../nih_plug_egui" } 17 | 18 | atomic_float = "0.1" 19 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_egui/src/main.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | 3 | use gain_gui_egui::Gain; 4 | 5 | fn main() { 6 | nih_export_standalone::(); 7 | } 8 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_iced/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain_gui_iced" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "A simple gain plugin with an iced GUI" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 15 | nih_plug_iced = { path = "../../../nih_plug_iced" } 16 | 17 | atomic_float = "0.1" 18 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_iced/src/editor.rs: -------------------------------------------------------------------------------- 1 | use atomic_float::AtomicF32; 2 | use nih_plug::prelude::{util, Editor, GuiContext}; 3 | use nih_plug_iced::widgets as nih_widgets; 4 | use nih_plug_iced::*; 5 | use std::sync::Arc; 6 | use std::time::Duration; 7 | 8 | use crate::GainParams; 9 | 10 | // Makes sense to also define this here, makes it a bit easier to keep track of 11 | pub(crate) fn default_state() -> Arc { 12 | IcedState::from_size(200, 150) 13 | } 14 | 15 | pub(crate) fn create( 16 | params: Arc, 17 | peak_meter: Arc, 18 | editor_state: Arc, 19 | ) -> Option> { 20 | create_iced_editor::(editor_state, (params, peak_meter)) 21 | } 22 | 23 | struct GainEditor { 24 | params: Arc, 25 | context: Arc, 26 | 27 | peak_meter: Arc, 28 | 29 | gain_slider_state: nih_widgets::param_slider::State, 30 | peak_meter_state: nih_widgets::peak_meter::State, 31 | } 32 | 33 | #[derive(Debug, Clone, Copy)] 34 | enum Message { 35 | /// Update a parameter's value. 36 | ParamUpdate(nih_widgets::ParamMessage), 37 | } 38 | 39 | impl IcedEditor for GainEditor { 40 | type Executor = executor::Default; 41 | type Message = Message; 42 | type InitializationFlags = (Arc, Arc); 43 | 44 | fn new( 45 | (params, peak_meter): Self::InitializationFlags, 46 | context: Arc, 47 | ) -> (Self, Command) { 48 | let editor = GainEditor { 49 | params, 50 | context, 51 | 52 | peak_meter, 53 | 54 | gain_slider_state: Default::default(), 55 | peak_meter_state: Default::default(), 56 | }; 57 | 58 | (editor, Command::none()) 59 | } 60 | 61 | fn context(&self) -> &dyn GuiContext { 62 | self.context.as_ref() 63 | } 64 | 65 | fn update( 66 | &mut self, 67 | _window: &mut WindowQueue, 68 | message: Self::Message, 69 | ) -> Command { 70 | match message { 71 | Message::ParamUpdate(message) => self.handle_param_message(message), 72 | } 73 | 74 | Command::none() 75 | } 76 | 77 | fn view(&mut self) -> Element<'_, Self::Message> { 78 | Column::new() 79 | .align_items(Alignment::Center) 80 | .push( 81 | Text::new("Gain GUI") 82 | .font(assets::NOTO_SANS_LIGHT) 83 | .size(40) 84 | .height(50.into()) 85 | .width(Length::Fill) 86 | .horizontal_alignment(alignment::Horizontal::Center) 87 | .vertical_alignment(alignment::Vertical::Bottom), 88 | ) 89 | .push( 90 | Text::new("Gain") 91 | .height(20.into()) 92 | .width(Length::Fill) 93 | .horizontal_alignment(alignment::Horizontal::Center) 94 | .vertical_alignment(alignment::Vertical::Center), 95 | ) 96 | .push( 97 | nih_widgets::ParamSlider::new(&mut self.gain_slider_state, &self.params.gain) 98 | .map(Message::ParamUpdate), 99 | ) 100 | .push(Space::with_height(10.into())) 101 | .push( 102 | nih_widgets::PeakMeter::new( 103 | &mut self.peak_meter_state, 104 | util::gain_to_db(self.peak_meter.load(std::sync::atomic::Ordering::Relaxed)), 105 | ) 106 | .hold_time(Duration::from_millis(600)), 107 | ) 108 | .into() 109 | } 110 | 111 | fn background_color(&self) -> nih_plug_iced::Color { 112 | nih_plug_iced::Color { 113 | r: 0.98, 114 | g: 0.98, 115 | b: 0.98, 116 | a: 1.0, 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_vizia/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain_gui_vizia" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | description = "A simple gain plugin with an vizia GUI" 9 | 10 | [lib] 11 | # The `lib` artifact is needed for the standalone target 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [dependencies] 15 | nih_plug = { path = "../../../", features = ["standalone"] } 16 | nih_plug_vizia = { path = "../../../nih_plug_vizia" } 17 | 18 | atomic_float = "0.1" 19 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_vizia/src/editor.rs: -------------------------------------------------------------------------------- 1 | use atomic_float::AtomicF32; 2 | use nih_plug::prelude::{util, Editor}; 3 | use nih_plug_vizia::vizia::prelude::*; 4 | use nih_plug_vizia::widgets::*; 5 | use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; 6 | use std::sync::atomic::Ordering; 7 | use std::sync::Arc; 8 | use std::time::Duration; 9 | 10 | use crate::GainParams; 11 | 12 | #[derive(Lens)] 13 | struct Data { 14 | params: Arc, 15 | peak_meter: Arc, 16 | } 17 | 18 | impl Model for Data {} 19 | 20 | // Makes sense to also define this here, makes it a bit easier to keep track of 21 | pub(crate) fn default_state() -> Arc { 22 | ViziaState::new(|| (200, 150)) 23 | } 24 | 25 | pub(crate) fn create( 26 | params: Arc, 27 | peak_meter: Arc, 28 | editor_state: Arc, 29 | ) -> Option> { 30 | create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| { 31 | assets::register_noto_sans_light(cx); 32 | assets::register_noto_sans_thin(cx); 33 | 34 | Data { 35 | params: params.clone(), 36 | peak_meter: peak_meter.clone(), 37 | } 38 | .build(cx); 39 | 40 | VStack::new(cx, |cx| { 41 | Label::new(cx, "Gain GUI") 42 | .font_family(vec![FamilyOwned::Name(String::from(assets::NOTO_SANS))]) 43 | .font_weight(FontWeightKeyword::Thin) 44 | .font_size(30.0) 45 | .height(Pixels(50.0)) 46 | .child_top(Stretch(1.0)) 47 | .child_bottom(Pixels(0.0)); 48 | 49 | Label::new(cx, "Gain"); 50 | ParamSlider::new(cx, Data::params, |params| ¶ms.gain); 51 | 52 | PeakMeter::new( 53 | cx, 54 | Data::peak_meter 55 | .map(|peak_meter| util::gain_to_db(peak_meter.load(Ordering::Relaxed))), 56 | Some(Duration::from_millis(600)), 57 | ) 58 | // This is how adding padding works in vizia 59 | .top(Pixels(10.0)); 60 | }) 61 | .row_between(Pixels(0.0)) 62 | .child_left(Stretch(1.0)) 63 | .child_right(Stretch(1.0)); 64 | 65 | ResizeHandle::new(cx); 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /plugins/examples/gain_gui_vizia/src/main.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | 3 | use gain_gui_vizia::Gain; 4 | 5 | fn main() { 6 | nih_export_standalone::(); 7 | } 8 | -------------------------------------------------------------------------------- /plugins/examples/midi_inverter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "midi_inverter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | -------------------------------------------------------------------------------- /plugins/examples/poly_mod_synth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poly_mod_synth" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | 14 | rand = "0.8.5" 15 | rand_pcg = "0.3.1" 16 | -------------------------------------------------------------------------------- /plugins/examples/sine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sine" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | -------------------------------------------------------------------------------- /plugins/examples/stft/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stft" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | 14 | realfft = "3.0" 15 | -------------------------------------------------------------------------------- /plugins/examples/sysex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sysex" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "ISC" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | nih_plug = { path = "../../../", features = ["assert_process_allocs"] } 13 | -------------------------------------------------------------------------------- /plugins/examples/sysex/src/lib.rs: -------------------------------------------------------------------------------- 1 | use nih_plug::prelude::*; 2 | use std::sync::Arc; 3 | 4 | #[derive(Default)] 5 | struct SysEx { 6 | params: Arc, 7 | } 8 | 9 | #[derive(Default, Params)] 10 | struct SysExParams {} 11 | 12 | // This is a struct or enum describing all MIDI SysEx messages this plugin will send or receive 13 | #[derive(Debug, Clone, PartialEq)] 14 | enum CoolSysExMessage { 15 | Foo(f32), 16 | Bar { x: u8, y: u8 }, 17 | } 18 | 19 | // This trait is used to convert between `CoolSysExMessage` and the raw MIDI SysEx messages. That 20 | // way the rest of the code doesn't need to bother with parsing or SysEx implementation details. 21 | impl SysExMessage for CoolSysExMessage { 22 | // This is a byte array that is large enough to write all of the messages to 23 | type Buffer = [u8; 6]; 24 | 25 | fn from_buffer(buffer: &[u8]) -> Option { 26 | // `buffer` contains the entire buffer, including headers and the 0xf7 End Of system 27 | // eXclusive byte 28 | match buffer { 29 | [0xf0, 0x69, 0x01, n, 0xf7] => Some(CoolSysExMessage::Foo(*n as f32 / 127.0)), 30 | [0xf0, 0x69, 0x02, x, y, 0xf7] => Some(CoolSysExMessage::Bar { x: *x, y: *y }), 31 | _ => None, 32 | } 33 | } 34 | 35 | fn to_buffer(self) -> (Self::Buffer, usize) { 36 | // `Self::Buffer` needs to have a fixed size, so the result needs to be padded, and we 37 | // return the message's actual length in bytes alongside it so the caller can trim the 38 | // excess padding 39 | match self { 40 | CoolSysExMessage::Foo(x) => ([0xf0, 0x69, 0x01, (x * 127.0).round() as u8, 0xf7, 0], 5), 41 | CoolSysExMessage::Bar { x, y } => ([0xf0, 0x69, 0x02, x, y, 0xf7], 6), 42 | } 43 | } 44 | } 45 | 46 | impl Plugin for SysEx { 47 | const NAME: &'static str = "SysEx Example"; 48 | const VENDOR: &'static str = "Moist Plugins GmbH"; 49 | const URL: &'static str = "https://youtu.be/dQw4w9WgXcQ"; 50 | const EMAIL: &'static str = "info@example.com"; 51 | 52 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 53 | 54 | // This plugin doesn't have any audio IO 55 | const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[]; 56 | 57 | const SAMPLE_ACCURATE_AUTOMATION: bool = true; 58 | 59 | // The plugin needs to be have a note port to be able to send SysEx 60 | const MIDI_INPUT: MidiConfig = MidiConfig::Basic; 61 | const MIDI_OUTPUT: MidiConfig = MidiConfig::Basic; 62 | 63 | type SysExMessage = CoolSysExMessage; 64 | type BackgroundTask = (); 65 | 66 | fn params(&self) -> Arc { 67 | self.params.clone() 68 | } 69 | 70 | fn process( 71 | &mut self, 72 | _buffer: &mut Buffer, 73 | _aux: &mut AuxiliaryBuffers, 74 | context: &mut impl ProcessContext, 75 | ) -> ProcessStatus { 76 | // This example converts one of the two messages into the other 77 | while let Some(event) = context.next_event() { 78 | if let NoteEvent::MidiSysEx { timing, message } = event { 79 | let new_message = match message { 80 | CoolSysExMessage::Foo(x) => CoolSysExMessage::Bar { 81 | x: (x * 127.0).round() as u8, 82 | y: 69, 83 | }, 84 | CoolSysExMessage::Bar { x, y: _ } => CoolSysExMessage::Foo(x as f32 / 127.0), 85 | }; 86 | 87 | context.send_event(NoteEvent::MidiSysEx { 88 | timing, 89 | message: new_message, 90 | }); 91 | } 92 | } 93 | 94 | ProcessStatus::Normal 95 | } 96 | } 97 | 98 | impl ClapPlugin for SysEx { 99 | const CLAP_ID: &'static str = "com.moist-plugins-gmbh.sysex"; 100 | const CLAP_DESCRIPTION: Option<&'static str> = 101 | Some("An example plugin to demonstrate sending and receiving SysEx"); 102 | const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); 103 | const CLAP_SUPPORT_URL: Option<&'static str> = None; 104 | const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::NoteEffect, ClapFeature::Utility]; 105 | } 106 | 107 | impl Vst3Plugin for SysEx { 108 | const VST3_CLASS_ID: [u8; 16] = *b"SysExCoolPluginn"; 109 | const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ 110 | Vst3SubCategory::Fx, 111 | Vst3SubCategory::Instrument, 112 | Vst3SubCategory::Tools, 113 | ]; 114 | } 115 | 116 | nih_export_clap!(SysEx); 117 | nih_export_vst3!(SysEx); 118 | -------------------------------------------------------------------------------- /plugins/loudness_war_winner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loudness_war_winner" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/loudness_war_winner" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | -------------------------------------------------------------------------------- /plugins/loudness_war_winner/README.md: -------------------------------------------------------------------------------- 1 | # Loudness War Winner 2 | 3 | This plugin does what it says on the tin. Have you ever wanted to show off your 4 | dominance by winning the loudness war? Neither have I. Dissatisfaction 5 | guaranteed. 6 | 7 | _Loudness War Winner may cause side effects such as headache, nausea, diarrhea, 8 | loss of balance, loss of sight, loss of hearing, loss of other senses, loss of 9 | common sense, heart palpitations, heart arrhythmia, heart attack, other Demi 10 | Lovato songs, rashes, acne, boils, simmers, chronic earwax, acute belly button 11 | lint, athlete's foot, tennis elbow, couch potato butt, depression, anxiety, 12 | Scientology, multiple sclerosis, single sclerosis, divorced sclerosis, it's 13 | complicated sclerosis, paralysis, hyperactivity, TikTokism, time travel, sudden 14 | clarity and awareness of your surroundings and situation, pneumonia, juvenile 15 | diabetes, hysterical pregnancy, existential dread, and enlarged earlobes. Ask 16 | your doctor if Loudness War Winner is right for you._ 17 | 18 | ## Download 19 | 20 | You can download the development binaries for Linux, Windows and macOS from the 21 | [automated 22 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 23 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 24 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 25 | 26 | On macOS you may need to [disable 27 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 28 | more difficult to run unsigned code on macOS. 29 | 30 | ### Building 31 | 32 | After installing [Rust](https://rustup.rs/), you can compile the Loudness War 33 | Winner as follows: 34 | 35 | ```shell 36 | cargo xtask bundle loudness_war_winner --release 37 | ``` 38 | -------------------------------------------------------------------------------- /plugins/loudness_war_winner/src/filter.rs: -------------------------------------------------------------------------------- 1 | // Loudness War Winner: Because negative LUFS are boring 2 | // Copyright (C) 2022-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use nih_plug::debug::*; 18 | use std::f32::consts; 19 | use std::ops::{Add, Mul, Sub}; 20 | 21 | /// A simple biquad filter with functions for generating coefficients for second order low-pass and 22 | /// high-pass filters. 23 | /// 24 | /// Based on . 25 | /// 26 | /// The type parameter T should be either an `f32` or a SIMD type. 27 | #[derive(Clone, Copy, Debug)] 28 | pub struct Biquad { 29 | pub coefficients: BiquadCoefficients, 30 | s1: T, 31 | s2: T, 32 | } 33 | 34 | /// The coefficients `[b0, b1, b2, a1, a2]` for [`Biquad`]. These coefficients are all 35 | /// prenormalized, i.e. they have been divided by `a0`. 36 | /// 37 | /// The type parameter T should be either an `f32` or a SIMD type. 38 | #[derive(Clone, Copy, Debug)] 39 | pub struct BiquadCoefficients { 40 | b0: T, 41 | b1: T, 42 | b2: T, 43 | a1: T, 44 | a2: T, 45 | } 46 | 47 | /// Either an `f32` or some SIMD vector type of `f32`s that can be used with our biquads. 48 | pub trait SimdType: 49 | Mul + Sub + Add + Copy + Sized 50 | { 51 | fn from_f32(value: f32) -> Self; 52 | } 53 | 54 | impl Default for Biquad { 55 | /// Before setting constants the filter should just act as an identity function. 56 | fn default() -> Self { 57 | Self { 58 | coefficients: BiquadCoefficients::identity(), 59 | s1: T::from_f32(0.0), 60 | s2: T::from_f32(0.0), 61 | } 62 | } 63 | } 64 | 65 | impl Biquad { 66 | /// Process a single sample. 67 | pub fn process(&mut self, sample: T) -> T { 68 | let result = self.coefficients.b0 * sample + self.s1; 69 | 70 | self.s1 = self.coefficients.b1 * sample - self.coefficients.a1 * result + self.s2; 71 | self.s2 = self.coefficients.b2 * sample - self.coefficients.a2 * result; 72 | 73 | result 74 | } 75 | 76 | /// Reset the state to zero, useful after making making large, non-interpolatable changes to the 77 | /// filter coefficients. 78 | pub fn reset(&mut self) { 79 | self.s1 = T::from_f32(0.0); 80 | self.s2 = T::from_f32(0.0); 81 | } 82 | } 83 | 84 | impl BiquadCoefficients { 85 | /// Convert scalar coefficients into the correct vector type. 86 | pub fn from_f32s(scalar: BiquadCoefficients) -> Self { 87 | Self { 88 | b0: T::from_f32(scalar.b0), 89 | b1: T::from_f32(scalar.b1), 90 | b2: T::from_f32(scalar.b2), 91 | a1: T::from_f32(scalar.a1), 92 | a2: T::from_f32(scalar.a2), 93 | } 94 | } 95 | 96 | /// Filter coefficients that would cause the sound to be passed through as is. 97 | pub fn identity() -> Self { 98 | Self::from_f32s(BiquadCoefficients { 99 | b0: 1.0, 100 | b1: 0.0, 101 | b2: 0.0, 102 | a1: 0.0, 103 | a2: 0.0, 104 | }) 105 | } 106 | 107 | /// Compute the coefficients for a band-pass filter. 108 | /// 109 | /// Based on . 110 | pub fn bandpass(sample_rate: f32, frequency: f32, q: f32) -> Self { 111 | nih_debug_assert!(sample_rate > 0.0); 112 | nih_debug_assert!(frequency > 0.0); 113 | nih_debug_assert!(frequency < sample_rate / 2.0); 114 | nih_debug_assert!(q > 0.0); 115 | 116 | let omega0 = consts::TAU * (frequency / sample_rate); 117 | let cos_omega0 = omega0.cos(); 118 | let alpha = omega0.sin() / (2.0 * q); 119 | 120 | // We'll prenormalize everything with a0 121 | let a0 = 1.0 + alpha; 122 | let b0 = alpha / a0; 123 | let b1 = 0.0 / a0; 124 | let b2 = -alpha / a0; 125 | let a1 = (-2.0 * cos_omega0) / a0; 126 | let a2 = (1.0 - alpha) / a0; 127 | 128 | Self::from_f32s(BiquadCoefficients { b0, b1, b2, a1, a2 }) 129 | } 130 | } 131 | 132 | impl SimdType for f32 { 133 | #[inline(always)] 134 | fn from_f32(value: f32) -> Self { 135 | value 136 | } 137 | } 138 | 139 | // TODO: Add SIMD 140 | // impl SimdType for f32x2 { 141 | // #[inline(always)] 142 | // fn from_f32(value: f32) -> Self { 143 | // f32x2::splat(value) 144 | // } 145 | // } 146 | -------------------------------------------------------------------------------- /plugins/puberty_simulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "puberty_simulator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/puberty_simulator" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | 15 | realfft = "3.0" 16 | -------------------------------------------------------------------------------- /plugins/puberty_simulator/README.md: -------------------------------------------------------------------------------- 1 | # Puberty Simulator 2 | 3 | This patent pending One Weird Plugin simulates the male voice change during 4 | puberty! If it was not already obvious from that sentence, this plugin is a 5 | joke, but it might actually be useful (or at least interesting) in some 6 | situations. This plugin pitches the signal down an octave, but it also has the 7 | side effect of causing things to sound like a cracking voice or to make them 8 | sound slightly out of tune. 9 | 10 | ## Download 11 | 12 | You can download the development binaries for Linux, Windows and macOS from the 13 | [automated 14 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 15 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 16 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 17 | 18 | On macOS you may need to [disable 19 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 20 | more difficult to run unsigned code on macOS. 21 | 22 | ### Building 23 | 24 | After installing [Rust](https://rustup.rs/), you can compile Puberty Simulator 25 | as follows: 26 | 27 | ```shell 28 | cargo xtask bundle puberty_simulator --release 29 | ``` 30 | -------------------------------------------------------------------------------- /plugins/safety_limiter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic 7 | Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ### Added 12 | 13 | - Safety Limiter now logs occurrences of NaN and infinite values so they're 14 | easier to spot. These values already caused Safety Limiter to engage, but this 15 | makes it very easy to notice that something fishy is going on during 16 | development. 17 | -------------------------------------------------------------------------------- /plugins/safety_limiter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "safety_limiter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/safety_limiter" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | -------------------------------------------------------------------------------- /plugins/safety_limiter/README.md: -------------------------------------------------------------------------------- 1 | # Safety Limiter 2 | 3 | This plugin is a simple tool to prevent ear damage. As soon as there is a peak 4 | above 0 dBFS or the specified threshold, the plugin will cut over to playing SOS 5 | in Morse code, gradually fading out again when the input returns back to safe 6 | levels. The same thing happens if the input contains infinite or NaN values. 7 | Made for personal use during plugin development and intense sound design 8 | sessions, but maybe you'll find it useful too! 9 | 10 | **Why not use a regular brickwall peak limiter?** 11 | The downside of doing that is that you may end up mixing or doing sound design 12 | into that limiter without realizing it. And at that point, you'll probably need 13 | to either redo part of the process. So this plugin simply gives you a very 14 | non-subtle heads up instead of altering the sound. 15 | 16 | ## Download 17 | 18 | You can download the development binaries for Linux, Windows and macOS from the 19 | [automated 20 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 21 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 22 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 23 | 24 | On macOS you may need to [disable 25 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 26 | more difficult to run unsigned code on macOS. 27 | 28 | ### Building 29 | 30 | After installing [Rust](https://rustup.rs/), you can compile Safety Limiter as 31 | follows: 32 | 33 | ```shell 34 | cargo xtask bundle safety_limiter --release 35 | ``` 36 | -------------------------------------------------------------------------------- /plugins/soft_vacuum/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic 7 | Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | -------------------------------------------------------------------------------- /plugins/soft_vacuum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soft_vacuum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/soft_vacuum" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs"] } 14 | 15 | [dev-dependencies] 16 | approx = "0.5.1" 17 | -------------------------------------------------------------------------------- /plugins/soft_vacuum/README.md: -------------------------------------------------------------------------------- 1 | # Soft Vacuum (Airwindows port) 2 | 3 | This is a straightforward port of Airwindows' [Hard 4 | Vacuum](https://www.airwindows.com/hard-vacuum-vst/) plugin with parameter 5 | smoothing and up to 16x linear-phase oversampling, because I liked the 6 | distortion and just wished it had oversampling. All credit goes to Chris from 7 | Airwindows. I just wanted to share this in case anyone else finds it useful. 8 | 9 | ## Download 10 | 11 | You can download the development binaries for Linux, Windows and macOS from the 12 | [automated 13 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 14 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 15 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 16 | 17 | On macOS you may need to [disable 18 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 19 | more difficult to run unsigned code on macOS. 20 | 21 | ### Building 22 | 23 | After installing [Rust](https://rustup.rs/), you can compile Safety Limiter as 24 | follows: 25 | 26 | ```shell 27 | cargo xtask bundle soft_vacuum --release 28 | ``` 29 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic 7 | Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [0.4.3] - 2023-03-31 10 | 11 | ### Changed 12 | 13 | - The default window size has been changed to 2048 since this offers a slightly 14 | better tradeoff between faster timings and more spectral precision. Existing 15 | instances are not affected. 16 | 17 | ### Fixed 18 | 19 | - Fixed the soft-knee options in the sidechain matching mode. They previously 20 | didn't account for the changing compressor thresholds, which could result in 21 | unexpected loud volume spikes. 22 | - The sidechain matching mode now caps the relative thresholds to behave more 23 | consistently with quiet inputs. 24 | 25 | ## [0.4.2] - 2023-03-22 26 | 27 | ### Changed 28 | 29 | - Reworked the resetting behavior again to smoothly fade the timings in after a 30 | reset to allow the envelop followers to more gently settle in. 31 | 32 | ## [0.4.1] - 2023-03-22 33 | 34 | ### Fixed 35 | 36 | - Fixed a regression from version 0.4.0 that caused the envelope followers to 37 | always be stuck at their fastest timings. 38 | 39 | ## [0.4.0] - 2023-03-22 40 | 41 | ### Added 42 | 43 | - Added an analyzer that visualizes the target curve, the spectral envelope 44 | followers and gain reduction. The current version will be expanded a bit in a 45 | future with tooltips and labels to show more information. 46 | 47 | ### Changed 48 | 49 | - The envelope followers reset in a smarter way after the plugin resumes from 50 | sleep or when the window size has changed. This avoids loud spikes in these 51 | situations when using extreme compression settings and slow timings. 52 | - The default window overlap amount setting has changed to 16x. Existing patches 53 | are not affected. 54 | - On Windows, clicking on the plugin's name no longer takes you to Spectral 55 | Compressor's home page. This is a temporary workaround for an issue with an 56 | underlying library. 57 | 58 | ## [0.3.0] - 2023-01-15 59 | 60 | ### Added 61 | 62 | - Added the version number and a link to the GitHub page to the GUI to make it 63 | easier to determine which version you're using. 64 | 65 | ### Removed 66 | 67 | - The DC filter option is gone. It was used to prevent upwards compression from 68 | amplifying DC and very low subbass signals in the original Spectral 69 | Compressor, but this iteration of Spectral Compressor doesn't need it and the 70 | feature only caused confusion. 71 | 72 | ### Changed 73 | 74 | - The compression part of Spectral Compressor has been rewritten to have 75 | theoretically smoother and cleaner transfer curves and to be slightly more 76 | performant. 77 | - The downwards hi-freq rolloff parameter now correctly scales the ratios. 78 | Previously the parameter didn't do much. 79 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spectral_compressor" 3 | version = "0.4.3" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | license = "GPL-3.0-or-later" 7 | homepage = "https://github.com/robbert-vdh/nih-plug/tree/master/plugins/spectral_compressor" 8 | 9 | [lib] 10 | crate-type = ["lib", "cdylib"] 11 | 12 | [dependencies] 13 | nih_plug = { path = "../../", features = ["assert_process_allocs", "standalone"] } 14 | nih_plug_vizia = { path = "../../nih_plug_vizia" } 15 | 16 | realfft = "3.0" 17 | 18 | # For the GUI 19 | atomic_float = "0.1" 20 | crossbeam = "0.8" 21 | open = "3.0" 22 | serde = { version = "1.0", features = ["derive"] } 23 | triple_buffer = "6.2" 24 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/README.md: -------------------------------------------------------------------------------- 1 | # Spectral Compressor 2 | 3 | Have you ever wondered what a 16384 band OTT would sound like? Neither have I. 4 | Spectral Compressor can squash anything into pink noise, apply simultaneous 5 | upwards and downwards compressor to dynamically match the sidechain signal's 6 | spectrum and morph one sound into another, and lots more. 7 | 8 | ![Screenshot](https://i.imgur.com/r6WfoYp.png) 9 | 10 | This is a port of https://github.com/robbert-vdh/spectral-compressor with more 11 | features and much better performance. 12 | 13 | ## Download 14 | 15 | You can download the development binaries for Linux, Windows and macOS from the 16 | [automated 17 | builds](https://github.com/robbert-vdh/nih-plug/actions/workflows/build.yml?query=branch%3Amaster) 18 | page. Or if you're not signed in on GitHub, then you can also find the latest nightly 19 | build [here](https://nightly.link/robbert-vdh/nih-plug/workflows/build/master). 20 | 21 | On macOS you may need to [disable 22 | Gatekeeper](https://disable-gatekeeper.github.io/) as Apple has recently made it 23 | more difficult to run unsigned code on macOS. 24 | 25 | ### Building 26 | 27 | After installing [Rust](https://rustup.rs/), you can compile Spectral Compressor 28 | as follows: 29 | 30 | ```shell 31 | cargo xtask bundle spectral_compressor --release 32 | ``` 33 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/src/analyzer.rs: -------------------------------------------------------------------------------- 1 | // Spectral Compressor: an FFT based compressor 2 | // Copyright (C) 2021-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use crate::curve::CurveParams; 18 | 19 | /// The data stored used for the spectrum analyzer. This also contains the gain reduction and the 20 | /// threshold curve (which is dynamic in the sidechain matching mode). 21 | /// 22 | /// All of these values are raw gain/amplitude or dB values obtained directly from the DSP code. If 23 | /// this needs to be skewed for visualization then that should be done in the editor. 24 | /// 25 | /// This pulls the data directly from the spectral compression part of Spectral Compressor, so the 26 | /// window size and overlap amounts are equal to the ones used by SC's main algorithm. If the 27 | /// current window size is 2048, then only the first `2048 / 2 + 1` elements in the arrays are used. 28 | #[derive(Debug, Clone)] 29 | pub struct AnalyzerData { 30 | /// The parameters used for the global threshold curve. This is used to draw the same curve used 31 | /// by the compressors on the analyzer. 32 | pub curve_params: CurveParams, 33 | /// The upwards and downwards threshold offsets for the curve. These are used to draw the curve 34 | /// twice with some distance between them if either is non-zero. 35 | pub curve_offsets_db: (f32, f32), 36 | 37 | /// The number of used bins. This is part of the `AnalyzerData` since recomputing it in the 38 | /// editor could result in a race condition. 39 | pub num_bins: usize, 40 | /// The amplitudes of all frequency bins in a windowed FFT of Spectral Compressor's output. Also 41 | /// includes the DC offset bin which we don't draw, just to make this a bit less confusing. 42 | /// 43 | /// This data is taken directly from the envelope followers, so it has the same rise and fall 44 | /// time as what is used by the compressors. 45 | pub envelope_followers: [f32; crate::MAX_WINDOW_SIZE / 2 + 1], 46 | /// The gain different applied to each band, in decibels. Alternatively, the negative gain 47 | /// reduction. Positive values mean that a band becomes louder, and negative values mean a band 48 | /// got attenuated. Does not (and should not) factor in the output gain. 49 | pub gain_difference_db: [f32; crate::MAX_WINDOW_SIZE / 2 + 1], 50 | // TODO: Include the threshold curve. Decide on whether to only visualizer the 'global' 51 | // threshold curve or to also show the individual upwards/downwards thresholds. Or omit 52 | // this and implement it in a nicer way for the premium Spectral Compressor. 53 | } 54 | 55 | impl Default for AnalyzerData { 56 | fn default() -> Self { 57 | Self { 58 | curve_params: CurveParams::default(), 59 | curve_offsets_db: (0.0, 0.0), 60 | num_bins: 0, 61 | envelope_followers: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], 62 | gain_difference_db: [0.0; crate::MAX_WINDOW_SIZE / 2 + 1], 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/src/curve.rs: -------------------------------------------------------------------------------- 1 | //! Abstractions for the parameterized threshold curve. 2 | //! 3 | //! This was previously computed directly inside of the `CompressorBank` but this makes it easier to 4 | //! reuse it when drawing the GUI. 5 | 6 | /// Parameters for a curve, similar to the fields found in `ThresholdParams` but using plain floats 7 | /// instead of parameters. 8 | #[derive(Debug, Default, Clone, Copy)] 9 | pub struct CurveParams { 10 | /// The compressor threshold at the center frequency. When sidechaining is enabled, the input 11 | /// signal is gained by the inverse of this value. This replaces the input gain in the original 12 | /// Spectral Compressor. In the polynomial below, this is the intercept. 13 | pub intercept: f32, 14 | /// The center frqeuency for the target curve when sidechaining is not enabled. The curve is a 15 | /// polynomial `threshold_db + curve_slope*x + curve_curve*(x^2)` that evaluates to a decibel 16 | /// value, where `x = ln(center_frequency) - ln(bin_frequency)`. In other words, this is 17 | /// evaluated in the log/log domain for decibels and octaves. 18 | pub center_frequency: f32, 19 | /// The slope for the curve, in the log/log domain. See the polynomial above. 20 | pub slope: f32, 21 | /// The, uh, 'curve' for the curve, in the logarithmic domain. This is the third coefficient in 22 | /// the quadratic polynomial and controls the parabolic behavior. Positive values turn the curve 23 | /// into a v-shaped curve, while negative values attenuate everything outside of the center 24 | /// frequency. See the polynomial above. 25 | pub curve: f32, 26 | } 27 | 28 | /// Evaluates the quadratic threshold curve. This used to be calculated directly inside of the 29 | /// compressor bank since it's so simple, but the editor also needs to compute this so it makes 30 | /// sense to deduplicate it a bit. 31 | /// 32 | /// The curve is evaluated in log-log space (so with octaves being the independent variable and gain 33 | /// in decibels being the output of the equation). 34 | pub struct Curve<'a> { 35 | params: &'a CurveParams, 36 | /// The natural logarithm of [`CurveParams::cemter_frequency`]. 37 | ln_center_frequency: f32, 38 | } 39 | 40 | impl<'a> Curve<'a> { 41 | pub fn new(params: &'a CurveParams) -> Self { 42 | Self { 43 | params, 44 | ln_center_frequency: params.center_frequency.ln(), 45 | } 46 | } 47 | 48 | /// Evaluate the curve for the natural logarithm of the frequency value. This can be used as an 49 | /// optimization to avoid computing these logarithms all the time. 50 | #[inline] 51 | pub fn evaluate_ln(&self, ln_freq: f32) -> f32 { 52 | let offset = ln_freq - self.ln_center_frequency; 53 | self.params.intercept + (self.params.slope * offset) + (self.params.curve * offset * offset) 54 | } 55 | 56 | /// Evaluate the curve for a value in Hertz. 57 | #[inline] 58 | #[allow(unused)] 59 | pub fn evaluate_linear(&self, freq: f32) -> f32 { 60 | self.evaluate_ln(freq.ln()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/src/editor/mode_button.rs: -------------------------------------------------------------------------------- 1 | // Spectral Compressor: an FFT based compressor 2 | // Copyright (C) 2021-2024 Robbert van der Helm 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::sync::Arc; 18 | 19 | use crossbeam::atomic::AtomicCell; 20 | use nih_plug_vizia::vizia::prelude::*; 21 | use nih_plug_vizia::widgets::GuiContextEvent; 22 | 23 | use super::EditorMode; 24 | 25 | /// A custom toggleable button that allows changing between the collapsed and expanded editor modes. 26 | pub struct EditorModeButton { 27 | mode: Arc>, 28 | } 29 | 30 | impl EditorModeButton { 31 | /// Creates a new button bound to the editor mode setting. 32 | pub fn new(cx: &mut Context, lens: L, label: impl Res + Clone) -> Handle 33 | where 34 | L: Lens>>, 35 | T: ToString, 36 | { 37 | Self { mode: lens.get(cx) } 38 | .build(cx, |cx| { 39 | Label::new(cx, label).hoverable(false); 40 | }) 41 | .checked(lens.map(|v| v.load() == EditorMode::AnalyzerVisible)) 42 | // We'll pretend this is a param-button, so this class is used for assigning a unique 43 | // color 44 | .class("editor-mode") 45 | } 46 | } 47 | 48 | impl View for EditorModeButton { 49 | fn element(&self) -> Option<&'static str> { 50 | // Reuse the styling from param-button 51 | Some("param-button") 52 | } 53 | 54 | fn event(&mut self, cx: &mut EventContext, event: &mut Event) { 55 | event.map(|window_event, meta| match window_event { 56 | WindowEvent::MouseDown(MouseButton::Left) 57 | | WindowEvent::MouseDoubleClick(MouseButton::Left) 58 | | WindowEvent::MouseTripleClick(MouseButton::Left) => { 59 | let current_mode = self.mode.load(); 60 | let new_mode = match current_mode { 61 | EditorMode::Collapsed => EditorMode::AnalyzerVisible, 62 | EditorMode::AnalyzerVisible => EditorMode::Collapsed, 63 | }; 64 | self.mode.store(new_mode); 65 | 66 | // This uses the function stored in our `ViziaState` to declaratively resize the GUI 67 | // to the correct size 68 | cx.emit(GuiContextEvent::Resize); 69 | 70 | meta.consume(); 71 | } 72 | // Mouse scrolling is intentionally not implemented here since it could be very easy to 73 | // do that by accident and that would cause the window to jump all over the place 74 | _ => {} 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugins/spectral_compressor/src/editor/theme.css: -------------------------------------------------------------------------------- 1 | analyzer { 2 | border-color: #0a0a0a; 3 | border-width: 1px; 4 | color: #0a0a0a; 5 | } 6 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | //! Different contexts the plugin can use to make callbacks to the host in different...contexts. 2 | 3 | use std::fmt::Display; 4 | 5 | pub mod gui; 6 | pub mod init; 7 | pub mod process; 8 | 9 | // Contexts for more plugin-API specific features 10 | pub mod remote_controls; 11 | 12 | /// The currently active plugin API. This may be useful to display in an about screen in the 13 | /// plugin's GUI for debugging purposes. 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 15 | pub enum PluginApi { 16 | Clap, 17 | Standalone, 18 | Vst3, 19 | } 20 | 21 | impl Display for PluginApi { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | PluginApi::Clap => write!(f, "CLAP"), 25 | PluginApi::Standalone => write!(f, "standalone"), 26 | PluginApi::Vst3 => write!(f, "VST3"), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/context/init.rs: -------------------------------------------------------------------------------- 1 | //! A context passed during plugin initialization. 2 | 3 | use super::PluginApi; 4 | use crate::prelude::Plugin; 5 | 6 | /// Callbacks the plugin can make while it is being initialized. This is passed to the plugin during 7 | /// [`Plugin::initialize()`][crate::plugin::Plugin::initialize()]. 8 | // 9 | // # Safety 10 | // 11 | // The implementing wrapper needs to be able to handle concurrent requests, and it should perform 12 | // the actual callback within [MainThreadQueue::schedule_gui]. 13 | pub trait InitContext { 14 | /// Get the current plugin API. 15 | fn plugin_api(&self) -> PluginApi; 16 | 17 | /// Run a task directly on this thread. This ensures that the task has finished executing before 18 | /// the plugin finishes initializing. 19 | /// 20 | /// # Note 21 | /// 22 | /// There is no asynchronous alternative for this function as that may result in incorrect 23 | /// behavior when doing offline rendering. 24 | fn execute(&self, task: P::BackgroundTask); 25 | 26 | /// Update the current latency of the plugin. If the plugin is currently processing audio, then 27 | /// this may cause audio playback to be restarted. 28 | fn set_latency_samples(&self, samples: u32); 29 | 30 | /// Set the current voice **capacity** for this plugin (so not the number of currently active 31 | /// voices). This may only be called if 32 | /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] 33 | /// is set. `capacity` must be between 1 and the configured maximum capacity. Changing this at 34 | /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly 35 | /// monophonic modulation when dropping the capacity down to 1. 36 | fn set_current_voice_capacity(&self, capacity: u32); 37 | } 38 | -------------------------------------------------------------------------------- /src/context/remote_controls.rs: -------------------------------------------------------------------------------- 1 | //! A context for defining plugin-specific [remote 2 | //! pages](https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/remote-controls.h) 3 | //! for CLAP plugins. 4 | 5 | use crate::prelude::Param; 6 | 7 | /// A context for defining plugin-specific [remote 8 | /// pages](https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/remote-controls.h) 9 | /// for CLAP plugins. 10 | /// 11 | /// These pages can contain references to up to eight parameters, but if the plugin defines more 12 | /// parameters for a page then the pages are automatically split. 13 | pub trait RemoteControlsContext { 14 | type Section: RemoteControlsSection; 15 | 16 | /// Define a section containing one or more remote control pages. This can be used to group 17 | /// remote control pages together. For instance, because an oscillator has so many parameters 18 | /// that it needs to span multiple pages, or to group the parameters for both filters into a 19 | /// single section. 20 | fn add_section(&mut self, name: impl Into, f: impl FnOnce(&mut Self::Section)); 21 | } 22 | 23 | /// A section or group of parameter pages. Empty sections will not be visible when using the plugin. 24 | pub trait RemoteControlsSection { 25 | type Page: RemoteControlsPage; 26 | 27 | /// Add a named parameter page to the section. See the documentation of [`RemoteControlsPage`] 28 | /// for more information. 29 | fn add_page(&mut self, name: impl Into, f: impl FnOnce(&mut Self::Page)); 30 | } 31 | 32 | /// A page containing references to up to eight parameters. If the number of slots used exceeds 33 | /// eight, then the page is split automatically. In that case the split page will have indices 34 | /// appended to it. For example, the `Lengty Params Page` defining 16 parameters will become `Lengty 35 | /// Params Page 1` and `Lengthy Params Page 2`. 36 | pub trait RemoteControlsPage { 37 | // Add a reference to one of the plugin's parameters to the page. 38 | fn add_param(&mut self, param: &impl Param); 39 | 40 | // Add an empty space on the page. Can be useful for grouping and aligning parameters within a 41 | // page. 42 | fn add_spacer(&mut self); 43 | } 44 | -------------------------------------------------------------------------------- /src/event_loop.rs: -------------------------------------------------------------------------------- 1 | //! An internal event loop for spooling tasks to the/a GUI thread. 2 | 3 | use std::sync::Weak; 4 | 5 | mod background_thread; 6 | 7 | #[cfg(all(target_family = "unix", not(target_os = "macos")))] 8 | mod linux; 9 | #[cfg(target_os = "macos")] 10 | mod macos; 11 | #[cfg(target_os = "windows")] 12 | mod windows; 13 | 14 | pub(crate) use self::background_thread::BackgroundThread; 15 | 16 | #[cfg_attr(not(feature = "vst3"), allow(unused_imports))] 17 | #[cfg(all(target_family = "unix", not(target_os = "macos")))] 18 | pub(crate) use self::linux::LinuxEventLoop as OsEventLoop; 19 | #[cfg_attr(not(feature = "vst3"), allow(unused_imports))] 20 | #[cfg(target_os = "macos")] 21 | pub(crate) use self::macos::MacOSEventLoop as OsEventLoop; 22 | #[cfg_attr(not(feature = "vst3"), allow(unused_imports))] 23 | #[cfg(target_os = "windows")] 24 | pub(crate) use self::windows::WindowsEventLoop as OsEventLoop; 25 | 26 | // This needs to be pretty high to make sure parameter change events don't get dropped when there's 27 | // lots of automation/modulation going on 28 | pub(crate) const TASK_QUEUE_CAPACITY: usize = 4096; 29 | 30 | /// A trait describing the functionality of a platform-specific event loop that can execute tasks of 31 | /// type `T` in executor `E` on the operating system's main thread (if applicable). Posting a task 32 | /// to the internal task queue should be realtime-safe. This event loop should be created during the 33 | /// wrapper's initial initialization on the main thread. 34 | /// 35 | /// Additionally, this trait also allows posting tasks to a background thread that's completely 36 | /// detached from the GUI. This makes it possible for a plugin to execute long running jobs without 37 | /// blocking GUI rendering. 38 | /// 39 | /// This is never used generically, but having this as a trait will cause any missing functions on 40 | /// an implementation to show up as compiler errors even when using a different platform. And since 41 | /// the tasks and executor will be sent to a thread, they need to have static lifetimes. 42 | pub(crate) trait EventLoop 43 | where 44 | T: Send + 'static, 45 | E: MainThreadExecutor + 'static, 46 | { 47 | /// Create and start a new event loop. The thread this is called on will be designated as the 48 | /// main thread, so this should be called when constructing the wrapper. 49 | fn new_and_spawn(executor: Weak) -> Self; 50 | 51 | /// Either post the function to the task queue so it can be delegated to the main thread, or 52 | /// execute the task directly if this is the main thread. This function needs to be callable at 53 | /// any time without blocking. 54 | /// 55 | /// If the task queue is full, then this will return false. 56 | #[must_use] 57 | fn schedule_gui(&self, task: T) -> bool; 58 | 59 | /// Post a task to the background task queue so it can be run in a dedicated background thread 60 | /// without blocking the plugin's GUI. This function needs to be callable at any time without 61 | /// blocking. 62 | /// 63 | /// If the task queue is full, then this will return false. 64 | #[must_use] 65 | fn schedule_background(&self, task: T) -> bool; 66 | 67 | /// Whether the calling thread is the event loop's main thread. This is usually the thread the 68 | /// event loop instance was initialized on. 69 | fn is_main_thread(&self) -> bool; 70 | } 71 | 72 | /// Something that can execute tasks of type `T`. 73 | pub(crate) trait MainThreadExecutor: Send + Sync { 74 | /// Execute a task on the current thread. This is either called from the GUI thread or from 75 | /// another background thread, depending on how the task was scheduled in the [`EventContext`]. 76 | fn execute(&self, task: T, is_gui_thread: bool); 77 | } 78 | -------------------------------------------------------------------------------- /src/event_loop/linux.rs: -------------------------------------------------------------------------------- 1 | //! An event loop implementation for Linux. APIs on Linux are generally thread safe, so the context 2 | //! of a main thread does not exist there. Because of that, this mostly just serves as a way to 3 | //! delegate expensive processing to another thread. 4 | 5 | use std::sync::Weak; 6 | use std::thread::{self, ThreadId}; 7 | 8 | use super::{BackgroundThread, EventLoop, MainThreadExecutor}; 9 | use crate::util::permit_alloc; 10 | 11 | /// See [`EventLoop`][super::EventLoop]. 12 | pub(crate) struct LinuxEventLoop { 13 | /// The thing that ends up executing these tasks. The tasks are usually executed from the worker 14 | /// thread, but if the current thread is the main thread then the task cna also be executed 15 | /// directly. 16 | executor: Weak, 17 | 18 | /// The actual background thread. The implementation is shared with the background thread used 19 | /// in other backends. 20 | background_thread: BackgroundThread, 21 | 22 | /// The ID of the main thread. In practice this is the ID of the thread that created this task 23 | /// queue. 24 | main_thread_id: ThreadId, 25 | } 26 | 27 | impl EventLoop for LinuxEventLoop 28 | where 29 | T: Send + 'static, 30 | E: MainThreadExecutor + 'static, 31 | { 32 | fn new_and_spawn(executor: Weak) -> Self { 33 | Self { 34 | executor: executor.clone(), 35 | background_thread: BackgroundThread::get_or_create(executor), 36 | main_thread_id: thread::current().id(), 37 | } 38 | } 39 | 40 | fn schedule_gui(&self, task: T) -> bool { 41 | if self.is_main_thread() { 42 | match self.executor.upgrade() { 43 | Some(executor) => executor.execute(task, true), 44 | None => { 45 | nih_debug_assert_failure!("GUI task was posted after the executor was dropped") 46 | } 47 | } 48 | 49 | true 50 | } else { 51 | self.background_thread.schedule(task) 52 | } 53 | } 54 | 55 | fn schedule_background(&self, task: T) -> bool { 56 | // This event loop implementation already uses a thread that's completely decoupled from the 57 | // operating system's or the host's main thread, so we don't need _another_ thread here 58 | self.background_thread.schedule(task) 59 | } 60 | 61 | fn is_main_thread(&self) -> bool { 62 | // FIXME: `thread::current()` may allocate the first time it's called, is there a safe 63 | // non-allocating version of this without using huge OS-specific libraries? 64 | permit_alloc(|| thread::current().id() == self.main_thread_id) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/event_loop/macos.rs: -------------------------------------------------------------------------------- 1 | //! An event loop implementation for macOS. 2 | 3 | use core_foundation::base::kCFAllocatorDefault; 4 | use core_foundation::runloop::{ 5 | kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopRemoveSource, 6 | CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, 7 | CFRunLoopSourceSignal, CFRunLoopWakeUp, 8 | }; 9 | use crossbeam::channel::{self, Receiver, Sender}; 10 | use objc::{class, msg_send, sel, sel_impl}; 11 | use std::os::raw::c_void; 12 | use std::sync::Weak; 13 | 14 | use super::{BackgroundThread, EventLoop, MainThreadExecutor}; 15 | 16 | /// Wrapping the `CFRunLoopSourceRef` type is required to be able to annotate it as thread-safe. 17 | struct LoopSourceWrapper(CFRunLoopSourceRef); 18 | 19 | unsafe impl Send for LoopSourceWrapper {} 20 | unsafe impl Sync for LoopSourceWrapper {} 21 | 22 | /// See [`EventLoop`][super::EventLoop]. 23 | pub(crate) struct MacOSEventLoop { 24 | /// The thing that ends up executing these tasks. The tasks are usually executed from the worker 25 | /// thread, but if the current thread is the main thread then the task cna also be executed 26 | /// directly. 27 | executor: Weak, 28 | 29 | /// A background thread for running tasks independently from the host's GUI thread. Useful for 30 | /// longer, blocking tasks. 31 | background_thread: BackgroundThread, 32 | 33 | /// The reference to the run-loop source so that it can be torn down when this struct is 34 | /// dropped. 35 | loop_source: LoopSourceWrapper, 36 | 37 | /// The sender for passing messages to the main thread. 38 | main_thread_sender: Sender, 39 | 40 | /// The data that is passed to the external run loop source callback function via a raw pointer. 41 | /// The data is not accessed from the Rust side after creating it but it's kept here so as not 42 | /// to get dropped. 43 | _callback_data: Box<(Weak, Receiver)>, 44 | } 45 | 46 | impl EventLoop for MacOSEventLoop 47 | where 48 | T: Send + 'static, 49 | E: MainThreadExecutor + 'static, 50 | { 51 | fn new_and_spawn(executor: Weak) -> Self { 52 | let (main_thread_sender, main_thread_receiver) = 53 | channel::bounded::(super::TASK_QUEUE_CAPACITY); 54 | 55 | let callback_data = Box::new((executor.clone(), main_thread_receiver)); 56 | 57 | let loop_source; 58 | unsafe { 59 | let source_context = CFRunLoopSourceContext { 60 | info: &*callback_data as *const _ as *mut c_void, 61 | cancel: None, 62 | copyDescription: None, 63 | equal: None, 64 | hash: None, 65 | perform: loop_source_callback::, 66 | release: None, 67 | retain: None, 68 | schedule: None, 69 | version: 0, 70 | }; 71 | 72 | loop_source = CFRunLoopSourceCreate( 73 | kCFAllocatorDefault, 74 | 1, 75 | &source_context as *const _ as *mut CFRunLoopSourceContext, 76 | ); 77 | CFRunLoopAddSource(CFRunLoopGetMain(), loop_source, kCFRunLoopCommonModes); 78 | } 79 | 80 | Self { 81 | executor: executor.clone(), 82 | background_thread: BackgroundThread::get_or_create(executor), 83 | loop_source: LoopSourceWrapper(loop_source), 84 | main_thread_sender, 85 | _callback_data: callback_data, 86 | } 87 | } 88 | 89 | fn schedule_gui(&self, task: T) -> bool { 90 | if self.is_main_thread() { 91 | match self.executor.upgrade() { 92 | Some(executor) => executor.execute(task, true), 93 | None => nih_debug_assert_failure!("GUI task posted after the executor was dropped"), 94 | } 95 | 96 | true 97 | } else { 98 | // Only signal the main thread callback to be called if the task was added to the queue. 99 | let success = self.main_thread_sender.try_send(task).is_ok(); 100 | if success { 101 | unsafe { 102 | CFRunLoopSourceSignal(self.loop_source.0); 103 | CFRunLoopWakeUp(CFRunLoopGetMain()); 104 | } 105 | } 106 | 107 | success 108 | } 109 | } 110 | 111 | fn schedule_background(&self, task: T) -> bool { 112 | self.background_thread.schedule(task) 113 | } 114 | 115 | fn is_main_thread(&self) -> bool { 116 | unsafe { msg_send![class!(NSThread), isMainThread] } 117 | } 118 | } 119 | 120 | impl Drop for MacOSEventLoop { 121 | fn drop(&mut self) { 122 | unsafe { 123 | CFRunLoopRemoveSource( 124 | CFRunLoopGetMain(), 125 | self.loop_source.0, 126 | kCFRunLoopCommonModes, 127 | ); 128 | CFRunLoopSourceInvalidate(self.loop_source.0); 129 | } 130 | } 131 | } 132 | 133 | extern "C" fn loop_source_callback(info: *const c_void) 134 | where 135 | T: Send + 'static, 136 | E: MainThreadExecutor + 'static, 137 | { 138 | let (executor, receiver) = unsafe { &*(info as *mut (Weak, Receiver)) }; 139 | let executor = match executor.upgrade() { 140 | Some(executor) => executor, 141 | None => { 142 | nih_debug_assert_failure!("GUI task was posted after the executor was dropped"); 143 | return; 144 | } 145 | }; 146 | 147 | while let Ok(task) = receiver.try_recv() { 148 | executor.execute(task, true); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/midi/sysex.rs: -------------------------------------------------------------------------------- 1 | //! Traits for working with MIDI SysEx data. 2 | 3 | use std::borrow::{Borrow, BorrowMut}; 4 | use std::fmt::Debug; 5 | 6 | /// A type that can be converted to and from byte buffers containing MIDI SysEx messages. 7 | /// 8 | /// # SysEx buffers 9 | /// 10 | /// For maximum flexibility this trait works with RAW MIDI messages. This means that status bytes 11 | /// and end of SysEx (EOX) bytes are included in the input, and should also be included in the 12 | /// output. A consequence of this is that it is also possible to support system common and system 13 | /// real time messages as needed, as long as the plugin API supports those. 14 | /// 15 | /// For example, the message to turn general MIDI mode on is `[0xf0, 0x7e, 0x7f, 0x09, 0x01, 0xf7]`, 16 | /// and has a length of 6 bytes. Note that this includes the `0xf0` start byte and `0xf7` end byte. 17 | pub trait SysExMessage: Debug + Clone + PartialEq + Send + Sync { 18 | /// The byte array buffer the messages are read from and serialized to. Should be a `[u8; N]`, 19 | /// where `N` is the maximum supported message length in bytes. This covers the full message, 20 | /// see the trait's docstring for more information. 21 | /// 22 | /// Ideally this could just be a const generic but Rust doesn't let you use those as array 23 | /// lengths just yet. 24 | /// 25 | /// 26 | type Buffer: Borrow<[u8]> + BorrowMut<[u8]>; 27 | 28 | /// Read a SysEx message from `buffer` and convert it to this message type if supported. This 29 | /// covers the full message, see the trait's docstring for more information. `buffer`'s length 30 | /// matches the received message. It is not padded to match [`Buffer`][Self::Buffer]. 31 | fn from_buffer(buffer: &[u8]) -> Option; 32 | 33 | /// Serialize this message object as a SysEx message in a byte buffer. This returns a buffer 34 | /// alongside the message's length in bytes. The buffer may contain padding at the end. This 35 | /// should contain the full message including headers and the EOX byte, see the trait's 36 | /// docstring for more information. 37 | fn to_buffer(self) -> (Self::Buffer, usize); 38 | } 39 | 40 | /// A default implementation plugins that don't need SysEx support can use. 41 | impl SysExMessage for () { 42 | type Buffer = [u8; 0]; 43 | 44 | fn from_buffer(_buffer: &[u8]) -> Option { 45 | None 46 | } 47 | 48 | fn to_buffer(self) -> (Self::Buffer, usize) { 49 | ([], 0) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/plugin/clap.rs: -------------------------------------------------------------------------------- 1 | use super::Plugin; 2 | use crate::prelude::{ClapFeature, RemoteControlsContext}; 3 | 4 | /// Provides auxiliary metadata needed for a CLAP plugin. 5 | #[allow(unused_variables)] 6 | pub trait ClapPlugin: Plugin { 7 | /// A unique ID that identifies this particular plugin. This is usually in reverse domain name 8 | /// notation, e.g. `com.manufacturer.plugin-name`. 9 | const CLAP_ID: &'static str; 10 | /// An optional short description for the plugin. 11 | const CLAP_DESCRIPTION: Option<&'static str>; 12 | /// The URL to the plugin's manual, if available. 13 | const CLAP_MANUAL_URL: Option<&'static str>; 14 | /// The URL to the plugin's support page, if available. 15 | const CLAP_SUPPORT_URL: Option<&'static str>; 16 | /// Keywords describing the plugin. The host may use this to classify the plugin in its plugin 17 | /// browser. 18 | const CLAP_FEATURES: &'static [ClapFeature]; 19 | 20 | /// If set, this informs the host about the plugin's capabilities for polyphonic modulation. 21 | const CLAP_POLY_MODULATION_CONFIG: Option = None; 22 | 23 | /// This function can be implemented to define plugin-specific [remote control 24 | /// pages](https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/remote-controls.h) 25 | /// that the host can use to provide better hardware mapping for a plugin. See the linked 26 | /// extension for more information. 27 | fn remote_controls(&self, context: &mut impl RemoteControlsContext) {} 28 | } 29 | 30 | /// Configuration for the plugin's polyphonic modulation options, if it supports . 31 | pub struct PolyModulationConfig { 32 | /// The maximum number of voices this plugin will ever use. Call the context's 33 | /// `set_current_voice_capacity()` method during initialization or audio processing to set the 34 | /// polyphony limit. 35 | pub max_voice_capacity: u32, 36 | /// If set to `true`, then the host may send note events for the same channel and key, but using 37 | /// different voice IDs. Bitwig Studio, for instance, can use this to do voice stacking. After 38 | /// enabling this, you should always prioritize using voice IDs to map note events to voices. 39 | pub supports_overlapping_voices: bool, 40 | } 41 | -------------------------------------------------------------------------------- /src/plugin/vst3.rs: -------------------------------------------------------------------------------- 1 | use super::Plugin; 2 | use crate::prelude::Vst3SubCategory; 3 | 4 | /// Provides auxiliary metadata needed for a VST3 plugin. 5 | pub trait Vst3Plugin: Plugin { 6 | /// The unique class ID that identifies this particular plugin. You can use the 7 | /// `*b"fooofooofooofooo"` syntax for this. 8 | /// 9 | /// This will be shuffled into a different byte order on Windows for project-compatibility. 10 | const VST3_CLASS_ID: [u8; 16]; 11 | /// One or more subcategories. The host may use these to categorize the plugin. Internally this 12 | /// slice will be converted to a string where each character is separated by a pipe character 13 | /// (`|`). This string has a limit of 127 characters, and anything longer than that will be 14 | /// truncated. 15 | const VST3_SUBCATEGORIES: &'static [Vst3SubCategory]; 16 | 17 | /// [`VST3_CLASS_ID`][Self::VST3_CLASS_ID`] in the correct order for the current platform so 18 | /// projects and presets can be shared between platforms. This should not be overridden. 19 | const PLATFORM_VST3_CLASS_ID: [u8; 16] = swap_vst3_uid_byte_order(Self::VST3_CLASS_ID); 20 | } 21 | 22 | #[cfg(not(target_os = "windows"))] 23 | const fn swap_vst3_uid_byte_order(uid: [u8; 16]) -> [u8; 16] { 24 | uid 25 | } 26 | 27 | #[cfg(target_os = "windows")] 28 | const fn swap_vst3_uid_byte_order(mut uid: [u8; 16]) -> [u8; 16] { 29 | // No mutable references in const functions, so we can't use `uid.swap()` 30 | let original_uid = uid; 31 | 32 | uid[0] = original_uid[3]; 33 | uid[1] = original_uid[2]; 34 | uid[2] = original_uid[1]; 35 | uid[3] = original_uid[0]; 36 | 37 | uid[4] = original_uid[5]; 38 | uid[5] = original_uid[4]; 39 | uid[6] = original_uid[7]; 40 | uid[7] = original_uid[6]; 41 | 42 | uid 43 | } 44 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Used in [`AudioIOLayout`] 2 | pub use std::num::NonZeroU32; 3 | 4 | // Re-export the macros, derive macros are already re-exported from their respective modules 5 | pub use crate::debug::*; 6 | 7 | pub use crate::nih_export_clap; 8 | #[cfg(feature = "vst3")] 9 | pub use crate::nih_export_vst3; 10 | #[cfg(feature = "standalone")] 11 | pub use crate::wrapper::standalone::{nih_export_standalone, nih_export_standalone_with_args}; 12 | 13 | pub use crate::formatters; 14 | pub use crate::util; 15 | 16 | pub use crate::audio_setup::{ 17 | new_nonzero_u32, AudioIOLayout, AuxiliaryBuffers, BufferConfig, PortNames, ProcessMode, 18 | }; 19 | pub use crate::buffer::Buffer; 20 | pub use crate::context::gui::{AsyncExecutor, GuiContext, ParamSetter}; 21 | pub use crate::context::init::InitContext; 22 | pub use crate::context::process::{ProcessContext, Transport}; 23 | pub use crate::context::remote_controls::{ 24 | RemoteControlsContext, RemoteControlsPage, RemoteControlsSection, 25 | }; 26 | pub use crate::context::PluginApi; 27 | // This also includes the derive macro 28 | pub use crate::editor::{Editor, ParentWindowHandle}; 29 | pub use crate::midi::sysex::SysExMessage; 30 | pub use crate::midi::{control_change, MidiConfig, NoteEvent, PluginNoteEvent}; 31 | pub use crate::params::enums::{Enum, EnumParam}; 32 | pub use crate::params::internals::ParamPtr; 33 | pub use crate::params::range::{FloatRange, IntRange}; 34 | pub use crate::params::smoothing::{AtomicF32, Smoothable, Smoother, SmoothingStyle}; 35 | pub use crate::params::Params; 36 | pub use crate::params::{BoolParam, FloatParam, IntParam, Param, ParamFlags}; 37 | pub use crate::plugin::clap::{ClapPlugin, PolyModulationConfig}; 38 | #[cfg(feature = "vst3")] 39 | pub use crate::plugin::vst3::Vst3Plugin; 40 | pub use crate::plugin::{Plugin, ProcessStatus, TaskExecutor}; 41 | pub use crate::wrapper::clap::features::ClapFeature; 42 | pub use crate::wrapper::state::PluginState; 43 | #[cfg(feature = "vst3")] 44 | pub use crate::wrapper::vst3::subcategories::Vst3SubCategory; 45 | -------------------------------------------------------------------------------- /src/util/window.rs: -------------------------------------------------------------------------------- 1 | //! Windowing functions, useful in conjunction with [`StftHelper`][super::StftHelper]. 2 | 3 | use std::f32; 4 | 5 | /// A Blackman window function with the 'standard' coefficients. 6 | /// 7 | /// 8 | pub fn blackman(size: usize) -> Vec { 9 | let mut window = vec![0.0; size]; 10 | blackman_in_place(&mut window); 11 | 12 | window 13 | } 14 | 15 | /// The same as [`blackman()`], but filling an existing slice instead. asfasdf 16 | pub fn blackman_in_place(window: &mut [f32]) { 17 | let size = window.len(); 18 | 19 | let scale_1 = (2.0 * f32::consts::PI) / (size - 1) as f32; 20 | let scale_2 = scale_1 * 2.0; 21 | for (i, sample) in window.iter_mut().enumerate() { 22 | let cos_1 = (scale_1 * i as f32).cos(); 23 | let cos_2 = (scale_2 * i as f32).cos(); 24 | *sample = 0.42 - (0.5 * cos_1) + (0.08 * cos_2); 25 | } 26 | } 27 | 28 | /// A Hann window function. 29 | /// 30 | /// 31 | pub fn hann(size: usize) -> Vec { 32 | let mut window = vec![0.0; size]; 33 | hann_in_place(&mut window); 34 | 35 | window 36 | } 37 | 38 | /// The same as [`hann()`], but filling an existing slice instead. 39 | pub fn hann_in_place(window: &mut [f32]) { 40 | let size = window.len(); 41 | 42 | // We want to scale `[0, size - 1]` to `[0, pi]`. 43 | // XXX: The `sin^2()` version results in weird rounding errors that cause spectral leakage 44 | let scale = (size as f32 - 1.0).recip() * f32::consts::TAU; 45 | for (i, sample) in window.iter_mut().enumerate() { 46 | let cos = (i as f32 * scale).cos(); 47 | *sample = 0.5 - (0.5 * cos) 48 | } 49 | } 50 | 51 | /// Multiply a buffer with a window function. 52 | #[inline] 53 | pub fn multiply_with_window(buffer: &mut [f32], window_function: &[f32]) { 54 | // TODO: ALso use SIMD here if available 55 | for (sample, window_sample) in buffer.iter_mut().zip(window_function) { 56 | *sample *= window_sample; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/wrapper.rs: -------------------------------------------------------------------------------- 1 | //! Wrappers for different plugin types. Each wrapper has an entry point macro that you can pass the 2 | //! name of a type that implements `Plugin` to. The macro will handle the rest. 3 | 4 | pub mod clap; 5 | pub mod state; 6 | pub(crate) mod util; 7 | 8 | #[cfg(feature = "standalone")] 9 | pub mod standalone; 10 | #[cfg(feature = "vst3")] 11 | pub mod vst3; 12 | 13 | // This is used by the wrappers. 14 | pub use util::setup_logger; 15 | -------------------------------------------------------------------------------- /src/wrapper/clap/descriptor.rs: -------------------------------------------------------------------------------- 1 | use clap_sys::plugin::clap_plugin_descriptor; 2 | use clap_sys::version::CLAP_VERSION; 3 | use std::ffi::{CStr, CString}; 4 | use std::os::raw::c_char; 5 | 6 | use crate::prelude::ClapPlugin; 7 | 8 | /// A static descriptor for a plugin. This is used in both the descriptor and on the plugin object 9 | /// itself. 10 | /// 11 | /// This cannot be cloned as [`Self::clap_features_ptrs`] contains pointers to 12 | /// [`Self::clap_features`]. 13 | pub struct PluginDescriptor { 14 | // We need [CString]s for all of `ClapPlugin`'s `&str` fields 15 | clap_id: CString, 16 | name: CString, 17 | vendor: CString, 18 | url: CString, 19 | version: CString, 20 | clap_manual_url: Option, 21 | clap_support_url: Option, 22 | clap_description: Option, 23 | clap_features: Vec, 24 | 25 | /// The contains pointers to the strings in `clap_features`. 26 | clap_features_ptrs: Vec<*const c_char>, 27 | /// We only support a single plugin per descriptor right now, so we'll fill in the plugin 28 | /// descriptor upfront. We also need to initialize the `CString` fields above first before we 29 | /// can initialize this plugin descriptor. 30 | plugin_descriptor: Option, 31 | } 32 | 33 | unsafe impl Send for PluginDescriptor {} 34 | unsafe impl Sync for PluginDescriptor {} 35 | 36 | impl PluginDescriptor { 37 | /// Construct the plugin descriptor for a specific CLAP plugin. 38 | pub fn for_plugin() -> Self { 39 | let mut descriptor = Self { 40 | clap_id: CString::new(P::CLAP_ID).expect("`CLAP_ID` contained null bytes"), 41 | name: CString::new(P::NAME).expect("`NAME` contained null bytes"), 42 | vendor: CString::new(P::VENDOR).expect("`VENDOR` contained null bytes"), 43 | url: CString::new(P::URL).expect("`URL` contained null bytes"), 44 | version: CString::new(P::VERSION).expect("`VERSION` contained null bytes"), 45 | clap_manual_url: P::CLAP_MANUAL_URL 46 | .map(|url| CString::new(url).expect("`CLAP_MANUAL_URL` contained null bytes")), 47 | clap_support_url: P::CLAP_SUPPORT_URL 48 | .map(|url| CString::new(url).expect("`CLAP_SUPPORT_URL` contained null bytes")), 49 | clap_description: P::CLAP_DESCRIPTION.map(|description| { 50 | CString::new(description).expect("`CLAP_DESCRIPTION` contained null bytes") 51 | }), 52 | clap_features: P::CLAP_FEATURES 53 | .iter() 54 | .map(|feat| feat.as_str()) 55 | .map(|s| CString::new(s).expect("`CLAP_FEATURES` contained null bytes")) 56 | .collect(), 57 | 58 | // These need to be initialized later as they contain pointers to the fields in this 59 | // descriptor 60 | clap_features_ptrs: Vec::new(), 61 | plugin_descriptor: None, 62 | }; 63 | 64 | // The keyword list is an environ-like list of char pointers terminated by a null pointer. 65 | descriptor.clap_features_ptrs = descriptor 66 | .clap_features 67 | .iter() 68 | .map(|feature| feature.as_ptr()) 69 | .collect(); 70 | descriptor.clap_features_ptrs.push(std::ptr::null()); 71 | 72 | // We couldn't initialize this directly because of all the CStrings 73 | // NOTE: This is safe without pinning this struct because all of the data is already stored 74 | // on the heap 75 | descriptor.plugin_descriptor = Some(clap_plugin_descriptor { 76 | clap_version: CLAP_VERSION, 77 | id: descriptor.clap_id.as_ptr(), 78 | name: descriptor.name.as_ptr(), 79 | vendor: descriptor.vendor.as_ptr(), 80 | url: descriptor.url.as_ptr(), 81 | version: descriptor.version.as_ptr(), 82 | manual_url: descriptor 83 | .clap_manual_url 84 | .as_ref() 85 | .map(|url| url.as_ptr()) 86 | .unwrap_or(std::ptr::null()), 87 | support_url: descriptor 88 | .clap_support_url 89 | .as_ref() 90 | .map(|url| url.as_ptr()) 91 | .unwrap_or(std::ptr::null()), 92 | description: descriptor 93 | .clap_description 94 | .as_ref() 95 | .map(|description| description.as_ptr()) 96 | .unwrap_or(std::ptr::null()), 97 | features: descriptor.clap_features_ptrs.as_ptr(), 98 | }); 99 | 100 | descriptor 101 | } 102 | 103 | pub fn clap_plugin_descriptor(&self) -> &clap_plugin_descriptor { 104 | self.plugin_descriptor.as_ref().unwrap() 105 | } 106 | 107 | pub fn clap_id(&self) -> &CStr { 108 | self.clap_id.as_c_str() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/wrapper/clap/features.rs: -------------------------------------------------------------------------------- 1 | //! Features a plugin supports. This is essentially the same thing as tags, keyword, or categories. 2 | //! Hosts may use these to organize plugins. 3 | 4 | /// A keyword for a CLAP plugin. See 5 | /// for more 6 | /// information. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 | pub enum ClapFeature { 9 | // These are the main categories, every plugin should have at least one of these 10 | Instrument, 11 | AudioEffect, 12 | NoteDetector, 13 | NoteEffect, 14 | // These are optional 15 | Analyzer, 16 | Synthesizer, 17 | Sampler, 18 | Drum, 19 | DrumMachine, 20 | Filter, 21 | Phaser, 22 | Equalizer, 23 | Deesser, 24 | PhaseVocoder, 25 | Granular, 26 | FrequencyShifter, 27 | PitchShifter, 28 | Distortion, 29 | TransientShaper, 30 | Compressor, 31 | Expander, 32 | Gate, 33 | Limiter, 34 | Flanger, 35 | Chorus, 36 | Delay, 37 | Reverb, 38 | Tremolo, 39 | Glitch, 40 | Utility, 41 | PitchCorrection, 42 | Restoration, 43 | MultiEffects, 44 | Mixing, 45 | Mastering, 46 | Mono, 47 | Stereo, 48 | Surround, 49 | Ambisonic, 50 | /// A non-predefined feature. Hosts may display this among its plugin categories. Custom 51 | /// features _must_ be prefixed by a namespace in the format `namespace:feature_name`. 52 | Custom(&'static str), 53 | } 54 | 55 | impl ClapFeature { 56 | pub fn as_str(&self) -> &'static str { 57 | match self { 58 | ClapFeature::Instrument => "instrument", 59 | ClapFeature::AudioEffect => "audio-effect", 60 | ClapFeature::NoteDetector => "note-detector", 61 | ClapFeature::NoteEffect => "note-effect", 62 | ClapFeature::Analyzer => "analyzer", 63 | ClapFeature::Synthesizer => "synthesizer", 64 | ClapFeature::Sampler => "sampler", 65 | ClapFeature::Drum => "drum", 66 | ClapFeature::DrumMachine => "drum-machine", 67 | ClapFeature::Filter => "filter", 68 | ClapFeature::Phaser => "phaser", 69 | ClapFeature::Equalizer => "equalizer", 70 | ClapFeature::Deesser => "de-esser", 71 | ClapFeature::PhaseVocoder => "phase-vocoder", 72 | ClapFeature::Granular => "granular", 73 | ClapFeature::FrequencyShifter => "frequency-shifter", 74 | ClapFeature::PitchShifter => "pitch-shifter", 75 | ClapFeature::Distortion => "distortion", 76 | ClapFeature::TransientShaper => "transient-shaper", 77 | ClapFeature::Compressor => "compressor", 78 | ClapFeature::Expander => "expander", 79 | ClapFeature::Gate => "gate", 80 | ClapFeature::Limiter => "limiter", 81 | ClapFeature::Flanger => "flanger", 82 | ClapFeature::Chorus => "chorus", 83 | ClapFeature::Delay => "delay", 84 | ClapFeature::Reverb => "reverb", 85 | ClapFeature::Tremolo => "tremolo", 86 | ClapFeature::Glitch => "glitch", 87 | ClapFeature::Utility => "utility", 88 | ClapFeature::PitchCorrection => "pitch-correction", 89 | ClapFeature::Restoration => "restoration", 90 | ClapFeature::MultiEffects => "multi-effects", 91 | ClapFeature::Mixing => "mixing", 92 | ClapFeature::Mastering => "mastering", 93 | ClapFeature::Mono => "mono", 94 | ClapFeature::Stereo => "stereo", 95 | ClapFeature::Surround => "surround", 96 | ClapFeature::Ambisonic => "ambisonic", 97 | ClapFeature::Custom(s) => { 98 | // Custom features must be prefixed with a namespace. We'll use `.split(':').all()` 99 | // here instead of `.split_once()` in case the user for whatever reason uses more 100 | // than one colon (which the docs don't say anything about, but uh yeah). 101 | nih_debug_assert!( 102 | s.contains(':') && s.split(':').all(|x| !x.is_empty()), 103 | "'{s}' is not a valid feature, custom features must be namespaced (e.g. \ 104 | 'nih:{s}')", 105 | s = s 106 | ); 107 | 108 | s 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/wrapper/standalone/backend.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::{AuxiliaryBuffers, PluginNoteEvent, Transport}; 2 | 3 | mod cpal; 4 | mod dummy; 5 | mod jack; 6 | 7 | pub use self::cpal::CpalMidir; 8 | pub use self::dummy::Dummy; 9 | pub use self::jack::Jack; 10 | pub use crate::buffer::Buffer; 11 | pub use crate::plugin::Plugin; 12 | 13 | /// An audio+MIDI backend for the standalone wrapper. 14 | pub trait Backend: 'static + Send + Sync { 15 | /// Start processing audio and MIDI on this thread. The process callback will be called whenever 16 | /// there's a new block of audio to be processed. The process callback receives the audio 17 | /// buffers for the wrapped plugin's outputs. Any inputs will have already been copied to this 18 | /// buffer. This will block until the process callback returns `false`. 19 | fn run( 20 | &mut self, 21 | cb: impl FnMut( 22 | &mut Buffer, 23 | &mut AuxiliaryBuffers, 24 | Transport, 25 | &[PluginNoteEvent

], 26 | &mut Vec>, 27 | ) -> bool 28 | + 'static 29 | + Send, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/wrapper/util/context_checks.rs: -------------------------------------------------------------------------------- 1 | //! Common validation used for the [contexts][crate::context]. 2 | 3 | use std::collections::HashSet; 4 | 5 | /// Ensures that parameter changes send from the GUI are wrapped in parameter gestures, and that the 6 | /// gestures are handled consistently (no duplicate starts and ends, no end before start, etc.). 7 | /// 8 | /// Should only be used in debug builds. 9 | #[derive(Debug, Default)] 10 | pub struct ParamGestureChecker { 11 | /// The parameters with an active gesture. 12 | active_params: HashSet, 13 | } 14 | 15 | impl Drop for ParamGestureChecker { 16 | fn drop(&mut self) { 17 | nih_debug_assert!( 18 | self.active_params.is_empty(), 19 | "GuiContext::end_set_parameter() was never called for {} {} {:?}", 20 | self.active_params.len(), 21 | if self.active_params.len() == 1 { 22 | "parameter" 23 | } else { 24 | "parameters" 25 | }, 26 | self.active_params 27 | ); 28 | } 29 | } 30 | 31 | impl ParamGestureChecker { 32 | /// Called for 33 | /// [`GuiContext::begin_set_parameter()`][crate::prelude::GuiContext::begin_set_parameter()]. 34 | /// Triggers a debug assertion failure if the state is inconsistent. 35 | pub fn begin_set_parameter(&mut self, param_id: &str) { 36 | nih_debug_assert!( 37 | !self.active_params.contains(param_id), 38 | "GuiContext::begin_set_parameter() was called twice for parameter '{}'", 39 | param_id 40 | ); 41 | self.active_params.insert(param_id.to_owned()); 42 | } 43 | 44 | /// Called for [`GuiContext::set_parameter()`][crate::prelude::GuiContext::set_parameter()]. 45 | /// Triggers a debug assertion failure if the state is inconsistent. 46 | pub fn set_parameter(&self, param_id: &str) { 47 | nih_debug_assert!( 48 | self.active_params.contains(param_id), 49 | "GuiContext::set_parameter() was called for parameter '{}' without a preceding \ 50 | begin_set_parameter() call", 51 | param_id 52 | ); 53 | } 54 | 55 | /// Called for 56 | /// [`GuiContext::end_set_parameter()`][crate::prelude::GuiContext::end_set_parameter()]. 57 | /// Triggers a debug assertion failure if the state is inconsistent. 58 | pub fn end_set_parameter(&mut self, param_id: &str) { 59 | nih_debug_assert!( 60 | self.active_params.contains(param_id), 61 | "GuiContext::end_set_parameter() was called for parameter '{}' without a preceding \ 62 | begin_set_parameter() call", 63 | param_id 64 | ); 65 | self.active_params.remove(param_id); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/wrapper/vst3/factory.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for building a VST3 factory. 2 | //! 3 | //! In order to support exporting multiple VST3 plugins from a single library a lot of functionality 4 | //! had to be moved to the `nih_export_vst3!()` macro. Because working in macro-land can be both 5 | //! frustrating and error prone, most code that does not specifically depend on all of the exposed 6 | //! plugin types was moved back to this module so it can be compiled and type checked as normal. 7 | 8 | use vst3_sys::base::{ 9 | ClassCardinality, FactoryFlags, PClassInfo, PClassInfo2, PClassInfoW, PFactoryInfo, 10 | }; 11 | 12 | use super::subcategories::Vst3SubCategory; 13 | use crate::prelude::Vst3Plugin; 14 | use crate::wrapper::util::strlcpy; 15 | use crate::wrapper::vst3::util::u16strlcpy; 16 | 17 | /// The VST3 SDK version this is roughly based on. The bindings include some VST 3.7 things but not 18 | /// everything, so we'll play it safe. 19 | pub const VST3_SDK_VERSION: &str = "VST 3.6.14"; 20 | 21 | /// The information queried about a plugin through the `IPluginFactory*` methods. Stored in a 22 | /// separate struct so it can be type erased and stored in an array. 23 | #[derive(Debug)] 24 | pub struct PluginInfo { 25 | pub cid: &'static [u8; 16], 26 | pub name: &'static str, 27 | pub subcategories: String, 28 | pub vendor: &'static str, 29 | pub version: &'static str, 30 | 31 | // These are used for the factory's own info struct 32 | pub url: &'static str, 33 | pub email: &'static str, 34 | } 35 | 36 | impl PluginInfo { 37 | pub fn for_plugin() -> PluginInfo { 38 | PluginInfo { 39 | cid: &P::PLATFORM_VST3_CLASS_ID, 40 | name: P::NAME, 41 | subcategories: make_subcategories_string::

(), 42 | vendor: P::VENDOR, 43 | version: P::VERSION, 44 | url: P::URL, 45 | email: P::EMAIL, 46 | } 47 | } 48 | 49 | /// Fill a [`PFactoryInfo`] struct with the information from this library. Used in 50 | /// `IPluginFactory`. 51 | pub fn create_factory_info(&self) -> PFactoryInfo { 52 | let mut info: PFactoryInfo = unsafe { std::mem::zeroed() }; 53 | strlcpy(&mut info.vendor, self.vendor); 54 | strlcpy(&mut info.url, self.url); 55 | strlcpy(&mut info.email, self.email); 56 | info.flags = FactoryFlags::kUnicode as i32; 57 | 58 | info 59 | } 60 | 61 | /// Fill a [`PClassInfo`] struct with the information from this library. Used in 62 | /// `IPluginFactory`. 63 | pub fn create_class_info(&self) -> PClassInfo { 64 | let mut info: PClassInfo = unsafe { std::mem::zeroed() }; 65 | info.cid.data = *self.cid; 66 | info.cardinality = ClassCardinality::kManyInstances as i32; 67 | strlcpy(&mut info.category, "Audio Module Class"); 68 | strlcpy(&mut info.name, self.name); 69 | 70 | info 71 | } 72 | 73 | /// Fill a [`PClassInfo2`] struct with the information from this library. Used in 74 | /// `IPluginFactory2`. 75 | pub fn create_class_info_2(&self) -> PClassInfo2 { 76 | let mut info: PClassInfo2 = unsafe { std::mem::zeroed() }; 77 | info.cid.data = *self.cid; 78 | info.cardinality = ClassCardinality::kManyInstances as i32; 79 | strlcpy(&mut info.category, "Audio Module Class"); 80 | strlcpy(&mut info.name, self.name); 81 | info.class_flags = 1 << 1; // kSimpleModeSupported 82 | strlcpy(&mut info.subcategories, &self.subcategories); 83 | strlcpy(&mut info.vendor, self.vendor); 84 | strlcpy(&mut info.version, self.version); 85 | strlcpy(&mut info.sdk_version, VST3_SDK_VERSION); 86 | 87 | info 88 | } 89 | 90 | /// Fill a [`PClassInfoW`] struct with the information from this library. Used in 91 | /// `IPluginFactory3`. 92 | pub fn create_class_info_unicode(&self) -> PClassInfoW { 93 | let mut info: PClassInfoW = unsafe { std::mem::zeroed() }; 94 | info.cid.data = *self.cid; 95 | info.cardinality = ClassCardinality::kManyInstances as i32; 96 | strlcpy(&mut info.category, "Audio Module Class"); 97 | u16strlcpy(&mut info.name, self.name); 98 | info.class_flags = 1 << 1; // kSimpleModeSupported 99 | strlcpy(&mut info.subcategories, &self.subcategories); 100 | u16strlcpy(&mut info.vendor, self.vendor); 101 | u16strlcpy(&mut info.version, self.version); 102 | u16strlcpy(&mut info.sdk_version, VST3_SDK_VERSION); 103 | 104 | info 105 | } 106 | } 107 | 108 | /// Build a pipe separated subcategories string for a VST3 plugin. 109 | fn make_subcategories_string() -> String { 110 | // No idea if any hosts do something with OnlyRT, but it's part of VST3's example categories 111 | // list. Plugins should not be adding this feature manually 112 | nih_debug_assert!(!P::VST3_SUBCATEGORIES.contains(&Vst3SubCategory::Custom("OnlyRT"))); 113 | let subcategory_string = P::VST3_SUBCATEGORIES 114 | .iter() 115 | .map(Vst3SubCategory::as_str) 116 | .collect::>() 117 | .join("|"); 118 | 119 | let subcategory_string = if P::HARD_REALTIME_ONLY { 120 | format!("{subcategory_string}|OnlyRT") 121 | } else { 122 | subcategory_string 123 | }; 124 | nih_debug_assert!(subcategory_string.len() <= 127); 125 | 126 | subcategory_string 127 | } 128 | -------------------------------------------------------------------------------- /src/wrapper/vst3/subcategories.rs: -------------------------------------------------------------------------------- 1 | //! Subcategories for VST3 plugins. This is essentially the same thing as tags, keyword, or 2 | //! categories. Hosts may use these to organize plugins. 3 | 4 | /// A subcategory for a VST3 plugin. See 5 | /// 6 | /// for a list of all predefined subcategories. Multiple subcategories are concatenated to a string 7 | /// separated by pipe characters, and the total length of this string may not exceed 127 characters. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | pub enum Vst3SubCategory { 10 | // These are the main categories, every plugin should have at least one of these, I think 11 | Fx, 12 | Instrument, 13 | Spatial, 14 | // These are optional 15 | Analyzer, 16 | Delay, 17 | Distortion, 18 | Drum, 19 | Dynamics, 20 | Eq, 21 | External, 22 | Filter, 23 | Generator, 24 | Mastering, 25 | Modulation, 26 | Network, 27 | Piano, 28 | PitchShift, 29 | Restoration, 30 | Reverb, 31 | Sampler, 32 | Synth, 33 | Tools, 34 | UpDownmix, 35 | // These are used for plugins that _only_ support this channel configuration, they're also 36 | // optional 37 | Mono, 38 | Stereo, 39 | Surround, 40 | Ambisonics, 41 | // There are also a couple special 'Only*' subcategories that convey special information about 42 | // the plugin. The framework is responsible for adding these, and they shouldn't be added 43 | // manually. 44 | /// A non-predefined subcategory. Hosts may display this among its plugin categories. 45 | Custom(&'static str), 46 | } 47 | 48 | impl Vst3SubCategory { 49 | pub fn as_str(&self) -> &'static str { 50 | match self { 51 | Vst3SubCategory::Fx => "Fx", 52 | Vst3SubCategory::Instrument => "Instrument", 53 | Vst3SubCategory::Spatial => "Spatial", 54 | Vst3SubCategory::Analyzer => "Analyzer", 55 | Vst3SubCategory::Delay => "Delay", 56 | Vst3SubCategory::Distortion => "Distortion", 57 | Vst3SubCategory::Drum => "Drum", 58 | Vst3SubCategory::Dynamics => "Dynamics", 59 | Vst3SubCategory::Eq => "EQ", 60 | Vst3SubCategory::External => "External", 61 | Vst3SubCategory::Filter => "Filter", 62 | Vst3SubCategory::Generator => "Generator", 63 | Vst3SubCategory::Mastering => "Mastering", 64 | Vst3SubCategory::Modulation => "Modulation", 65 | Vst3SubCategory::Network => "Network", 66 | Vst3SubCategory::Piano => "Piano", 67 | Vst3SubCategory::PitchShift => "Pitch Shift", 68 | Vst3SubCategory::Restoration => "Restoration", 69 | Vst3SubCategory::Reverb => "Reverb", 70 | Vst3SubCategory::Sampler => "Sampler", 71 | Vst3SubCategory::Synth => "Synth", 72 | Vst3SubCategory::Tools => "Tools", 73 | Vst3SubCategory::UpDownmix => "Up-Downmix", 74 | Vst3SubCategory::Mono => "Mono", 75 | Vst3SubCategory::Stereo => "Stereo", 76 | Vst3SubCategory::Surround => "Surround", 77 | Vst3SubCategory::Ambisonics => "Ambisonics", 78 | Vst3SubCategory::Custom(s) => { 79 | nih_debug_assert!( 80 | !s.contains('|'), 81 | "'{}' contains a pipe character ('|'), which is not allowed", 82 | s 83 | ); 84 | 85 | s 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/wrapper/vst3/util.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::ops::Deref; 3 | use vst3_sys::interfaces::IUnknown; 4 | use vst3_sys::vst::TChar; 5 | use vst3_sys::ComInterface; 6 | use widestring::U16CString; 7 | 8 | /// When `Plugin::MIDI_INPUT` is set to `MidiConfig::MidiCCs` or higher then we'll register 130*16 9 | /// additional parameters to handle MIDI CCs, channel pressure, and pitch bend, in that order. 10 | /// vst3-sys doesn't expose these constants. 11 | pub const VST3_MIDI_CCS: u32 = 130; 12 | pub const VST3_MIDI_CHANNELS: u32 = 16; 13 | /// The number of parameters we'll need to register if the plugin accepts MIDI CCs. 14 | pub const VST3_MIDI_NUM_PARAMS: u32 = VST3_MIDI_CCS * VST3_MIDI_CHANNELS; 15 | /// The start of the MIDI CC parameter ranges. We'll print an assertion failure if any of the 16 | /// plugin's parameters overlap with this range. The mapping to a parameter index is 17 | /// `VST3_MIDI_PARAMS_START + (cc_idx + (channel * VST3_MIDI_CCS))`. 18 | pub const VST3_MIDI_PARAMS_START: u32 = VST3_MIDI_PARAMS_END - VST3_MIDI_NUM_PARAMS; 19 | /// The (exclusive) end of the MIDI CC parameter range. Anything above this is reserved by the host. 20 | pub const VST3_MIDI_PARAMS_END: u32 = 1 << 31; 21 | 22 | /// Early exit out of a VST3 function when one of the passed pointers is null 23 | macro_rules! check_null_ptr { 24 | ($ptr:expr $(, $ptrs:expr)* $(, )?) => { 25 | check_null_ptr_msg!("Null pointer passed to function", $ptr $(, $ptrs)*) 26 | }; 27 | } 28 | 29 | /// The same as [`check_null_ptr!`], but with a custom message. 30 | macro_rules! check_null_ptr_msg { 31 | ($msg:expr, $ptr:expr $(, $ptrs:expr)* $(, )?) => { 32 | if $ptr.is_null() $(|| $ptrs.is_null())* { 33 | nih_debug_assert_failure!($msg); 34 | return kInvalidArgument; 35 | } 36 | }; 37 | } 38 | 39 | /// The same as [`strlcpy()`], but for VST3's fun UTF-16 strings instead. 40 | pub fn u16strlcpy(dest: &mut [TChar], src: &str) { 41 | if dest.is_empty() { 42 | return; 43 | } 44 | 45 | let src_utf16 = match U16CString::from_str(src) { 46 | Ok(s) => s, 47 | Err(err) => { 48 | nih_debug_assert_failure!("Invalid UTF-16 string: {}", err); 49 | return; 50 | } 51 | }; 52 | let src_utf16_chars = src_utf16.as_slice(); 53 | let src_utf16_chars_signed: &[TChar] = 54 | unsafe { &*(src_utf16_chars as *const [u16] as *const [TChar]) }; 55 | 56 | // Make sure there's always room for a null terminator 57 | let copy_len = cmp::min(dest.len() - 1, src_utf16_chars_signed.len()); 58 | dest[..copy_len].copy_from_slice(&src_utf16_chars_signed[..copy_len]); 59 | dest[copy_len] = 0; 60 | } 61 | 62 | /// Send+Sync wrapper for these interface pointers. 63 | #[repr(transparent)] 64 | pub struct VstPtr { 65 | ptr: vst3_sys::VstPtr, 66 | } 67 | 68 | /// The same as [`VstPtr`] with shared semnatics, but for objects we defined ourself since `VstPtr` 69 | /// only works for interfaces. 70 | #[repr(transparent)] 71 | pub struct ObjectPtr { 72 | ptr: *const T, 73 | } 74 | 75 | impl Deref for VstPtr { 76 | type Target = vst3_sys::VstPtr; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.ptr 80 | } 81 | } 82 | 83 | impl Deref for ObjectPtr { 84 | type Target = T; 85 | 86 | fn deref(&self) -> &Self::Target { 87 | unsafe { &*self.ptr } 88 | } 89 | } 90 | 91 | impl From> for VstPtr { 92 | fn from(ptr: vst3_sys::VstPtr) -> Self { 93 | Self { ptr } 94 | } 95 | } 96 | 97 | impl From<&T> for ObjectPtr { 98 | /// Create a smart pointer for an existing reference counted object. 99 | fn from(obj: &T) -> Self { 100 | unsafe { obj.add_ref() }; 101 | Self { ptr: obj } 102 | } 103 | } 104 | 105 | impl Drop for ObjectPtr { 106 | fn drop(&mut self) { 107 | unsafe { (*self).release() }; 108 | } 109 | } 110 | 111 | /// SAFETY: Sharing these pointers across thread is s safe as they have internal atomic reference 112 | /// counting, so as long as a `VstPtr` handle exists the object will stay alive. 113 | unsafe impl Send for VstPtr {} 114 | unsafe impl Sync for VstPtr {} 115 | 116 | unsafe impl Send for ObjectPtr {} 117 | unsafe impl Sync for ObjectPtr {} 118 | 119 | #[cfg(test)] 120 | mod miri { 121 | use widestring::U16CStr; 122 | 123 | use super::*; 124 | 125 | #[test] 126 | fn u16strlcpy_normal() { 127 | let mut dest = [0; 256]; 128 | u16strlcpy(&mut dest, "Hello, world!"); 129 | 130 | assert_eq!( 131 | unsafe { U16CStr::from_ptr_str(dest.as_ptr() as *const u16) } 132 | .to_string() 133 | .unwrap(), 134 | "Hello, world!" 135 | ); 136 | } 137 | 138 | #[test] 139 | fn u16strlcpy_overflow() { 140 | let mut dest = [0; 6]; 141 | u16strlcpy(&mut dest, "Hello, world!"); 142 | 143 | assert_eq!( 144 | unsafe { U16CStr::from_ptr_str(dest.as_ptr() as *const u16) } 145 | .to_string() 146 | .unwrap(), 147 | "Hello" 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Robbert van der Helm "] 6 | description = "A helper for compiling and bundling plugins, can be invoked in this repo using `cargo xtask`" 7 | license = "ISC" 8 | 9 | [dependencies] 10 | nih_plug_xtask = { path = "../nih_plug_xtask" } 11 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> nih_plug_xtask::Result<()> { 2 | nih_plug_xtask::main() 3 | } 4 | --------------------------------------------------------------------------------