check-branding.sh could say what disagreed but not fix it, and its canonical block lived in its own header — so renaming meant hand-editing the checker, which is exactly the fragile step it exists to catch. branding.conf now holds the identity once. Both scripts read it and neither contains a branding literal anywhere, so a rename never has to rewrite its own tooling. brand-plugin.sh derives the package name and Rust types from the display name, validates them against what Cargo actually accepts, rewrites every tracked file longest-value-first (a short name can be a substring of a longer one), appends the retired spellings to STALE, and finishes by running the checker. A run that ends green is a rename that landed. Dry run unless --apply. It refuses three things worth refusing: - Package names Cargo rejects — digit-leading (`206 Prototype` derives to `206_prototype`), Rust keywords, built-in crate collisions. Verified against cargo init rather than assumed. - Moving CLAP_ID or VST3_CLASS_ID without --allow-identity-change. Hosts key saved projects off those; changing either orphans every existing session silently. The 16-byte class ID is *proposed*, never derived — it is a fixed-width frozen identity, so the choice stays human. - Development history in CLAP_DESCRIPTION. Hosts show it to users who never knew the codename, so any retired name or the repo codename appearing there is rejected. The checker gains the other half of its job: asserting canonical values are present where they belong, not only that retired ones are gone. Absence alone would pass a rename to the *wrong* new spelling. New positive assertions cover Cargo authors, Cargo.lock, the CI bundle command and NOTICE — every one of which the recent Bari 206 rename had to touch. Structural checks read their file locations from branding.conf so a plugin whose params outgrow src/lib.rs can point them elsewhere. Also fixes NOTICE naming the repo rather than the product, found by the new assertion, and excludes branding.conf from the stale sweep — it lists every retired spelling by definition, the same reason check-branding.sh was already excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
7.8 KiB
NIH-plug Template
Minimal scaffold for new NIH-plug audio plugins (Rust + CLAP/VST3 + egui).
Based on patterns from musiikki/codename-206.
Quick Start
# Clone this repo as your starting point
git clone http://192.168.2.199:3005/musiikki/nih-plug-template my-plugin
cd my-plugin
# Edit: src/lib.rs, Cargo.toml, bundler.toml — replace placeholder values
# See musiikki/codename-206 for a real-world example of how to fill these in
# Then verify the rename landed everywhere
sh check-branding.sh
Renaming
Identity is duplicated across Cargo.toml, bundler.toml, src/lib.rs, NOTICE,
Cargo.lock, the CI workflow and the docs. Nothing makes those agree, so a half-finished
rename builds a bundle under one name while CI bundles another.
branding.conf holds the canonical identity once. Two scripts read it:
sh brand-plugin.sh --display "Fuzz Box" # derive, validate, show the plan (dry run)
sh brand-plugin.sh --display "Fuzz Box" --apply
sh check-branding.sh # exit 0 = consistent, 1 = drift
brand-plugin.sh changes names. It derives the package name and Rust types from the
display name, validates them against what Cargo actually accepts, rewrites every tracked
file, appends the retired spellings to STALE, and finishes by running the checker — a run
that ends green is a rename that landed. It is a dry run unless you pass --apply.
check-branding.sh verifies them. Both directions: every canonical value is asserted
present where it belongs, and no retired spelling survives anywhere. Absence alone is not
enough — renaming to the wrong new spelling would pass a stale-sweep and still break CI.
Three things it will refuse or warn about, all learned the hard way:
- A package name Cargo rejects. Names cannot start with a digit (
206 Prototype→206_prototype, rejected), be a Rust keyword, or collide with a built-in crate. - Moving frozen identity.
CLAP_IDandVST3_CLASS_IDare how hosts recognise saved projects and presets. Change either and every existing session silently loses the plugin — the track comes up empty, with no error. Pick them once before the first release; after that they never move.--allow-identity-changeis required to touch them, and the helper proposes a valid 16-byte class ID rather than deriving one, because that choice is yours. - Development history in
CLAP_DESCRIPTION. Hosts show it to users who never knew your codename. Any retired name or the repo's own codename appearing there is rejected.
The repo name is deliberately not derived. It is fixed when the repo is created and keeps
its development codename while the plugin gets a product name; CODENAME in branding.conf
records it so it can be kept out of user-facing text.
Checks for files the template doesn't ship (a deploy.ps1, say) report as skip and start
enforcing once you add them.
Build
# Install Rust via rustup: https://rustup.rs
# No VST3 SDK needed — NIH-plug bundles its own bindings.
# Build + bundle the CLAP (default)
cargo xtask bundle nih_plug_template --release
# Add the VST3 as well — see the licensing note below before you do this
cargo xtask bundle nih_plug_template --release --features vst3
Output: target/bundled/<name>.clap (and <name>.vst3 with --features vst3), where
<name> is the name set in bundler.toml — not the crate name. Out of the box that
means target/bundled/Plugin Name.clap.
On Linux the egui editor needs the GUI development headers:
sudo apt install pkg-config libgl-dev libx11-dev libxcursor-dev libxcb1-dev \
libxcb-icccm4-dev libxcb-dri2-0-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
Deploy
FL Studio
Build with --features vst3 first if you want the VST3 (see the licensing note below).
FL Studio scans C:\Program Files\Common Files\VST3 by default, ignores directory junctions
(so a symlinked bundle is invisible to its scanner), and caches failed scans.
- Copy the
.vst3bundle to a folder FL scans (e.g.C:\Program Files\Common Files\VST3). - Copy the
.clapbundle to a folder FL scans (e.g.C:\Program Files\Common Files\CLAP). - In FL Studio: Options → Manage plugins → tick "Rescan previously failed plugins" → Find installed plugins, then search for your plugin name (set in
bundler.toml).
Linux
# CLAP
cp -r "target/bundled/Plugin Name.clap" ~/.clap/
# VST3 (only exists if you built with --features vst3)
cp -r "target/bundled/Plugin Name.vst3" ~/.vst3/
# Or globally:
# sudo cp -r "target/bundled/Plugin Name.clap" /usr/lib/clap/
# sudo cp -r "target/bundled/Plugin Name.vst3" /usr/lib/vst3/
Project Structure
Cargo.toml # Package deps + the opt-in `vst3` feature — NIH-plug pinned to codename-206's rev
bundler.toml # Bundle name for `cargo xtask bundle`
src/lib.rs # Plugin skeleton — params, DSP, and egui editor, with one example gain param
xtask/ # Bundler xtask (cargo xtask bundle)
.cargo/config.toml # xtask alias
branding.conf # Canonical identity — the one place names are declared
check-branding.sh # Asserts every name is present where it belongs and no old one survives
brand-plugin.sh # Renames the plugin everywhere, then verifies with the checker
.gitattributes # Pins *.sh to LF so Windows checkouts keep a valid shebang
.github/workflows/ # CI: branding check, both feature sets build, test, bundle
CI
.github/workflows/ci.yaml gates pushes and PRs on the branding check, a build of both
feature sets, cargo test, and a bundle — enough to catch a rename that only half-landed,
a cfg(feature = "vst3") gate that stopped compiling, or a bundler.toml that no longer
matches the crate name.
It targets runs-on: rust-audio, an org-scoped runner label. A plugin cloned into a
different org will not see that runner and its jobs will queue indefinitely rather than fail
— register a runner advertising the label for that org, or change runs-on.
Deliberately not covered: plugin performance. Realtime cost can only be measured meaningfully
against a real audio deadline, and Plugin::process() cannot be called from a test at all
(ProcessContext requires a Transport, whose constructor and fields are pub(crate) to
nih-plug). Benchmark your DSP types directly if you need it.
License
GPL-3.0-or-later. See LICENSE for the terms and NOTICE for third-party attribution.
Plugins cloned from this template inherit GPL-3.0-or-later, because the template's own code is under it. If you need a permissive plugin, this is the wrong starting point — write the skeleton yourself against NIH-plug directly, which is ISC.
Parts of src/lib.rs derive from Robbert van der Helm's NIH-plug cookiecutter template and
gain example, both ISC. ISC is GPL-compatible, so that notice is retained in NOTICE
rather than superseded.
VST3 and Steinberg
vst3-sys, which nih_export_vst3!() needs, is GPLv3-or-later — chosen by its own authors,
who describe their port as a derivative of Steinberg's SDK that "was not made in isolation."
That obligation is what makes a distributed VST3 build GPL-licensed, and it holds regardless
of any question about Steinberg's own rights. Being GPL-3.0-or-later already, this template
satisfies it.
No Steinberg SDK source is vendored or redistributed here, so there is nothing to accept at build time. Separately from copyright, "VST" is a trademark of Steinberg Media Technologies GmbH; describing a distributed product as a VST plugin is governed by their trademark terms. If you intend to distribute commercially under that name, that is the part worth legal advice.
CLAP carries none of this — no GPL dependency and no trademark encumbrance.