Compare commits

..

8 Commits

Author SHA1 Message Date
mikkeli 6dc67c0931 Merge branch 'feat/branding-checker-structural-names'
Extend the branding checker over descriptions and Rust type names, and fix
two bugs the tests for it exposed: a stale scan that silently passed
outside a git checkout, and a TODO count that broke once it reached zero.
2026-08-29 10:29:14 +09:00
mikkeli 3acab7bf7e feat: check descriptions and Rust type names for branding drift
The checker covered the identity strings a host sees, but not two other
kinds of template residue.

Descriptions: `Cargo.toml`'s `description` and `CLAP_DESCRIPTION` both
shipped as placeholders, and the latter surfaces in some host browsers.

Type names: `MyPlugin` and `PluginParams` appear at nine sites between
the struct definitions, the three trait impls, the two export macros and
the params field. Missing one is a compile error rather than silent drift,
so this is a convenience rather than a safety net - but naming the site
beats reading rustc, and the `Vst3Plugin` impl sits behind a cfg, so a
default build never reports it at all. Renaming the types is optional:
leave the canonical values alone and the checks stay green.

TODO markers are reported at a new informational level that never counts
toward pass/fail. They are legitimate while building, so failing on them
would just train you to ignore the run - but a finished plugin should not
quietly ship with five of them.

Two bugs surfaced while testing this:

- The stale scan drew its file list from `git ls-files`, which returns
  nothing outside a checkout. In an exported tarball every stale spelling
  was therefore reported absent - the section passed hardest exactly when
  it could see least. It now falls back to `find` and says which source
  it used.
- `grep -c` prints 0 and exits 1 when there are no matches, so the TODO
  count's `|| echo 0` appended a second zero and produced an unparseable
  value once a rename removed the last TODO.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:29:06 +09:00
mikkeli 26f3593b15 Merge branch 'fix/template-api-and-tooling'
Make the template actually usable: it did not compile, its build docs
described output paths the bundler never produces, and its declared
licence conflicted with the artifact its default build emitted.

Also brings over codename-206's branding checker, extended so that
renaming — the first thing anyone does with a template — verifies itself.
2026-08-29 10:25:32 +09:00
mikkeli 4fd38cce75 chore: check in Cargo.lock
Cargo.toml claims the pinned nih-plug rev makes builds reproducible, but
the rev only pins that one tree. Everything beneath it — roughly 200
crates.io dependencies — still resolved to whatever was newest at build
time, so two clones a week apart could compile different code.

A cdylib plugin is a final artifact rather than a library others depend
on, so the usual argument for leaving the lockfile out does not apply.
Checking it in also means CI builds exactly what was built locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:25:02 +09:00
mikkeli 1ddba77c28 feat: add a branding consistency checker
Adapted from musiikki/codename-206, where the same script guards a single
plugin. It matters more here: renaming is the first thing anyone does with
a template, and identity is duplicated across Cargo.toml, bundler.toml,
src/lib.rs and the docs with nothing keeping them in agreement. A
half-finished rename builds a bundle under one name while the docs and
deploy script look for another.

Changes from the codename-206 original:

- Checks for files this repo does not ship (deploy.ps1) report as `skip`
  rather than FAIL, and start enforcing once the file appears.
- STALE ships pre-seeded with this template's own placeholders, and each
  entry is skipped while it is still part of a canonical value. That makes
  renaming self-checking: entries arm themselves as you change the value
  they belong to, with no bookkeeping. Substring rather than equality,
  because placeholders nest — "your-domain" lives inside the canonical
  CLAP_ID.
- Covers identity this template carries that codename-206's did not:
  VENDOR, EMAIL, the Cargo homepage feeding Plugin::URL, that the `env!`
  indirection is still in place, and the README's bundle command.
- Asserts CLAP_ID is reverse-DNS safe. A stray capital or space there is
  not a compile error, it is a silent identity change between machines.

Two placeholder spellings had to move to avoid false positives, since a
checker that cries wolf trains you to ignore it: bundler.toml's comment
contained the literal "Plugin Name", and the CLAP_ID TODO spelled the
placeholder a third way that matched neither the constant nor the STALE
list. The TODO now illustrates the form instead of repeating a placeholder.

.gitattributes pins *.sh to LF so core.autocrlf=true on Windows cannot
check the script out with a CRLF shebang that dash rejects.

Verified three ways: clean template passes, a partial rename names every
drifted file, and a complete rename passes and still compiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:24:52 +09:00
mikkeli c6f2f32fb3 feat: make VST3 an opt-in cargo feature
Cargo.toml declared Apache-2.0 while the default build linked vst3-sys,
whose bindings are GPLv3 — so the artifact a fresh clone produced could
not legally be distributed under the licence the repo claimed. A template
should not hand that trap to every project cloned from it.

NIH-plug already anticipates this: its `vst3` feature exists, in upstream's
own words, "mostly for GPL-compliance reasons". Taking `default-features =
false` and re-exporting `vst3` as a local opt-in makes the default build
CLAP-only and genuinely Apache-2.0 clean, while `--features vst3` remains
one flag away for anyone who accepts GPLv3.

Gating the feature rather than relicensing keeps the decision with whoever
uses the template. Reversing it is a one-line default plus removing two
cfg attributes, where relicensing to GPLv3 now would commit every
downstream plugin to that choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:24:33 +09:00
mikkeli fc45d3eec0 docs: correct bundle output paths and add Linux prerequisites
Two things in the build instructions were wrong rather than merely thin.

The stated output paths (`nih_plug_template.vst3`) are not what the
bundler produces: `cargo xtask bundle` names artifacts from the `name` key
in bundler.toml, so a fresh clone actually yields `Plugin Name.vst3`.
Anyone following the deploy steps verbatim would copy a path that does not
exist. The VST3 artifact is also a directory bundle, so the `cp` needed
`-r` to work at all.

The egui editor pulls in baseview and x11, whose build scripts abort on a
missing `gl.pc` with a pkg-config error that says nothing about this
project. Naming the packages up front turns a confusing first-build
failure into a one-line fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:23:59 +09:00
mikkeli dd5cedfa92 fix: rewrite the plugin skeleton against the pinned NIH-plug API
The scaffold did not compile. src/lib.rs was written against an API that
does not exist at the pinned rev (f36931f):

- `impl Params` declared six methods that are not trait members
  (`as_slice`, `param_by_id`, `param_by_name` and their `_mut` pairs) over
  two types that appear nowhere in the crate (`AtomicParam`, `ParamId`).
  The real trait is `unsafe trait Params` with one required method,
  `param_map`, and is meant to be derived rather than hand-written.
- `type SysSysexMessage` misspells the associated type `SysExMessage`.
- `ChannelSamples` has no immutable `get`, so the passthrough loop's
  `*frame.get_mut(ch).unwrap() = *frame.get(ch).unwrap()` could not resolve.

Replaced with `#[derive(Params)]` over an example gain parameter, so the
skeleton demonstrates the smoother and dB formatters a real plugin needs
instead of leaving an empty struct that teaches nothing. The passthrough
loop becomes an actual gain application for the same reason.

`editor()` is wired to a real `create_egui_editor` window in the same
change rather than a follow-up: nih_plug_egui was already a declared
dependency that nothing used, and the persisted `editor_state` field the
editor needs is part of the same params struct being rewritten here.
Splitting them would leave a commit whose params struct exists only to
serve an editor that is not there yet.

Verified against a local checkout of nih-plug at the pinned rev rather
than from memory, and confirmed by building.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-29 10:23:37 +09:00
7 changed files with 2885 additions and 64 deletions
+4
View File
@@ -0,0 +1,4 @@
# Shell scripts must keep LF endings. core.autocrlf=true on Windows would
# otherwise check them out CRLF, which a strict POSIX sh (dash, CI containers)
# rejects at the shebang.
*.sh text eol=lf
Generated
+2534
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -10,6 +10,14 @@ license = "Apache-2.0"
[workspace]
members = ["xtask"]
[features]
# CLAP only by default. NIH-plug's VST3 bindings (vst3-sys) are GPLv3, so linking
# them makes the resulting binary GPLv3 regardless of what this package declares.
# Opt in explicitly — and relicense this crate — if you intend to ship a VST3:
# cargo xtask bundle nih_plug_template --release --features vst3
default = []
vst3 = ["nih_plug/vst3"]
[lib]
# `cdylib` is the plugin binary; `lib` (rlib) lets `cargo test` link the unit tests.
crate-type = ["cdylib", "lib"]
@@ -17,7 +25,8 @@ crate-type = ["cdylib", "lib"]
[dependencies]
# Pinned to the same commit as musiikki/codename-206.
# Update the `rev` when you want to update NIH-plug.
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "f36931f7af4646065488a9845d8f8c2f95252c23", features = ["assert_process_allocs"] }
# `default-features = false` drops nih_plug's default `vst3` feature; see [features] above.
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "f36931f7af4646065488a9845d8f8c2f95252c23", default-features = false, features = ["assert_process_allocs"] }
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "f36931f7af4646065488a9845d8f8c2f95252c23" }
[profile.release]
+70 -13
View File
@@ -13,24 +13,69 @@ 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
Branding is duplicated across `Cargo.toml`, `bundler.toml`, `src/lib.rs` and the docs,
and nothing makes those agree — so a half-finished rename builds a bundle under one name
while the docs and deploy script look for another.
`check-branding.sh` declares the identity once at the top and asserts every copy matches:
```bash
sh check-branding.sh # exit 0 = consistent, 1 = drift found
```
It covers the identity a host actually sees (package and bundle names, `Plugin::NAME`,
`VENDOR`, `EMAIL`, `CLAP_ID`, `VST3_CLASS_ID`, the descriptions) and the Rust type names
(`MyPlugin`, `PluginParams`) at every site they appear. Renaming the types is cosmetic —
no host ever sees them — but leaving `MyPlugin` in a shipped plugin is the kind of residue
that says nobody read the file. Leave them as-is and those checks simply stay green.
To rename: edit the canonical block at the top of the script, run it, and fix everything
it reports. The placeholder spellings are pre-seeded into its `STALE` list, and each one
arms itself as soon as you change the matching canonical value — so leftovers surface
without you having to track them. It also asserts `VST3_CLASS_ID` is exactly 16 bytes and
that `CLAP_ID` is reverse-DNS safe, both of which are otherwise a compile error or a
silent identity change between machines.
Checks for files the template doesn't ship (e.g. a `deploy.ps1`) report as `skip` and
start enforcing once you add them.
## Build
```bash
# Install Rust via rustup: https://rustup.rs
# No VST3 SDK needed — NIH-plug bundles its own bindings.
# Build + bundle the VST3 and CLAP
# 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/nih_plug_template.vst3` and `nih_plug_template.clap`
**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:
```bash
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.
@@ -41,25 +86,27 @@ FL Studio scans `C:\Program Files\Common Files\VST3` by default, **ignores direc
### Linux
```bash
# VST3
cp target/bundled/nih_plug_template.vst3 ~/.vst3/
# CLAP
cp target/bundled/nih_plug_template.clap ~/.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 target/bundled/nih_plug_template.vst3 /usr/lib/vst3/
# sudo cp target/bundled/nih_plug_template.clap /usr/lib/clap/
# 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 — NIH-plug pinned to codename-206's rev
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 — fill in params, DSP, editor
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
check-branding.sh # Asserts the plugin's identity strings agree everywhere (see Renaming)
.gitattributes # Pins *.sh to LF so Windows checkouts keep a valid shebang
```
## License
@@ -68,6 +115,16 @@ Apache-2.0
## VST3 Licensing Note
You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries.
NIH-plug's VST3 bindings are GPLv3; if you distribute, the plugin must also be GPLv3
(or you need a commercial Steinberg licence). CLAP has no such restriction.
NIH-plug's VST3 bindings (`vst3-sys`) are GPLv3. Linking them makes the resulting binary
GPLv3 regardless of what `Cargo.toml` declares, so **VST3 is behind an opt-in `vst3` feature
and this template builds CLAP only by default.** That keeps the default build consistent with
the Apache-2.0 licence in `Cargo.toml`/`LICENSE`.
If you want to ship a VST3:
1. Build with `--features vst3`.
2. Relicense your plugin as GPLv3 (or obtain a commercial Steinberg licence) and update
`Cargo.toml`, `LICENSE`, and the SPDX headers in `src/`.
3. Accept Steinberg's VST3 SDK licence.
CLAP has no such restriction.
+1 -1
View File
@@ -1,7 +1,7 @@
# Metadata for NIH-plug's `cargo xtask bundle <package>` bundler.
#
# [package_name]
# name = "Human Readable Plugin Name" # defaults to <package_name>
# name = "Human Readable Bundle Name" # defaults to <package_name>
[nih_plug_template]
name = "Plugin Name"
+207
View File
@@ -0,0 +1,207 @@
#!/bin/sh
# check-branding.sh - verify the plugin's branding/identity strings agree everywhere.
#
# Plugin branding is duplicated across the build manifest, the bundler manifest,
# the source, the docs and (once you add one) the deploy script. Nothing makes
# them agree, so a rename drifts silently: the bundle builds under one name and
# the deploy script looks for another. This asserts they match, and that the
# identity constants are legal.
#
# In a template this matters more than usual: renaming is the first thing you do
# with this repo, and it touches every file below. Run this immediately after,
# and the STALE machinery will list whatever you missed.
#
# Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found)
#
# ---------------------------------------------------------------------------
# CANONICAL IDENTITY - the single source of truth. Edit these to rename, then
# run this script; every failure it prints is a place still holding the old name.
# ---------------------------------------------------------------------------
PKG="nih_plug_template" # Cargo package name == bundler.toml table key
DISPLAY="Plugin Name" # bundle name -> "<DISPLAY>.vst3" / "<DISPLAY>.clap"
PLUGIN_NAME="Plugin Name" # Plugin::NAME - what the DAW displays
VENDOR="Your Name" # Plugin::VENDOR
EMAIL="your@email.com" # Plugin::EMAIL
HOMEPAGE="https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME" # Cargo homepage -> Plugin::URL
CLAP_ID="com.your-domain.plugin-name" # CLAP_ID - stable plugin identity
VST3_CLASS_ID="Unique16CharId!!" # VST3_CLASS_ID - stable identity, MUST be 16 bytes
DESCRIPTION="A minimal NIH-plug audio plugin" # Cargo package description
CLAP_DESCRIPTION="Your plugin description" # CLAP_DESCRIPTION - shown in some host browsers
# Rust type names. These never reach a host, so renaming them is cosmetic - but
# leaving "MyPlugin" in a shipped plugin is the sort of residue that says nobody
# read the file. Change these to make the checks below enforce your own names;
# leave them alone and they simply stay green.
STRUCT="MyPlugin" # the Plugin impl target
PARAMS_STRUCT="PluginParams" # the Params impl target
# Spellings that must no longer appear anywhere in tracked files. Seeded with
# this template's own placeholders, so that renaming is self-checking: each entry
# is skipped while it is still one of the canonical values above, and becomes a
# live check the moment you change that value. Add your own old names here after
# a later rename.
STALE="nih_plug_template
MyPlugin
PluginParams
A minimal NIH-plug audio plugin
Your plugin description
Plugin Name
Your Name
your@email.com
com.your-domain.plugin-name
Unique16CharId!!
YOUR-USERNAME
YOUR-PLUGIN-NAME
your-domain"
set -u
cd "$(dirname "$0")" || exit 1
pass=0; fail=0; skip=0
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$1"; [ $# -gt 1 ] && printf ' %s\n' "$2"; return 0; }
note() { skip=$((skip+1)); printf ' skip %s\n' "$1"; }
# Reported for awareness only - never counted, never fails the run.
info() { printf ' -- %s\n' "$1"; }
# has FILE PATTERN -> fixed-string search
has() { grep -qF "$2" "$1" 2>/dev/null; }
check() { # check LABEL FILE LITERAL
if [ ! -f "$2" ]; then bad "$1" "missing file: $2"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
# Same as check, but a missing file is "not applicable yet" rather than drift.
# The template ships without a deploy script; this starts checking once you add one.
opt_check() { # opt_check LABEL FILE LITERAL
if [ ! -f "$2" ]; then note "$1 ($2 not present)"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
printf 'branding check (canonical: %s / "%s")\n\n' "$PKG" "$DISPLAY"
printf 'build manifests\n'
check "Cargo.toml package name" Cargo.toml "name = \"$PKG\""
check "Cargo.toml homepage" Cargo.toml "homepage = \"$HOMEPAGE\""
check "Cargo.toml description" Cargo.toml "description = \"$DESCRIPTION\""
check "bundler.toml table key" bundler.toml "[$PKG]"
check "bundler.toml display name" bundler.toml "name = \"$DISPLAY\""
printf '\nsource identity (src/lib.rs)\n'
check "Plugin::NAME" src/lib.rs "const NAME: &'static str = \"$PLUGIN_NAME\""
check "Plugin::VENDOR" src/lib.rs "const VENDOR: &'static str = \"$VENDOR\""
check "Plugin::EMAIL" src/lib.rs "const EMAIL: &'static str = \"$EMAIL\""
check "CLAP_ID" src/lib.rs "const CLAP_ID: &'static str = \"$CLAP_ID\""
check "CLAP_DESCRIPTION" src/lib.rs "const CLAP_DESCRIPTION: Option<&'static str> = Some(\"$CLAP_DESCRIPTION\")"
check "VST3_CLASS_ID" src/lib.rs "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\""
# Plugin::URL is env!("CARGO_PKG_HOMEPAGE"), so the manifest is the only place
# it is spelled. Assert the indirection is still in place rather than hardcoded.
check "Plugin::URL from manifest" src/lib.rs "const URL: &'static str = env!(\"CARGO_PKG_HOMEPAGE\")"
# VST3 class IDs are a fixed 16-byte array. A shorter or longer literal is a
# compile error, but it is far clearer to say so here than to read rustc on it.
n=$(printf '%s' "$VST3_CLASS_ID" | wc -c | tr -d ' ')
if [ "$n" -eq 16 ]; then ok "VST3_CLASS_ID is 16 bytes"
else bad "VST3_CLASS_ID is 16 bytes" "got $n bytes: \"$VST3_CLASS_ID\""; fi
# CLAP IDs are reverse-DNS by convention and are compared verbatim by hosts, so
# a stray space or capital is a silent identity change between machines.
case "$CLAP_ID" in
*\ *|*[A-Z]*) bad "CLAP_ID is reverse-DNS safe" "no spaces or capitals allowed: \"$CLAP_ID\"" ;;
*.*) ok "CLAP_ID is reverse-DNS safe" ;;
*) bad "CLAP_ID is reverse-DNS safe" "expected dotted reverse-DNS, got: \"$CLAP_ID\"" ;;
esac
printf '\nstructural names (src/lib.rs)\n'
# Every site the type names appear. A rename that misses one is a compile error
# rather than silent drift, but naming the site is faster than reading rustc,
# and the Vst3Plugin impl sits behind a cfg so a default build never reports it.
check "struct $STRUCT" src/lib.rs "struct $STRUCT {"
check "impl Plugin" src/lib.rs "impl Plugin for $STRUCT {"
check "impl ClapPlugin" src/lib.rs "impl ClapPlugin for $STRUCT {"
check "impl Vst3Plugin" src/lib.rs "impl Vst3Plugin for $STRUCT {"
check "nih_export_clap!" src/lib.rs "nih_export_clap!($STRUCT);"
check "nih_export_vst3!" src/lib.rs "nih_export_vst3!($STRUCT);"
check "struct $PARAMS_STRUCT" src/lib.rs "struct $PARAMS_STRUCT {"
check "impl Default for params" src/lib.rs "impl Default for $PARAMS_STRUCT {"
check "params field type" src/lib.rs "params: Arc<$PARAMS_STRUCT>,"
printf '\ndocs\n'
opt_check "README bundle command" README.md "cargo xtask bundle $PKG"
printf '\ndeploy script\n'
opt_check "deploy.ps1 default package" deploy.ps1 "\$Package = \"$PKG\""
opt_check "deploy.ps1 VST3 bundle" deploy.ps1 "\$vst3Name = \"$DISPLAY.vst3\""
opt_check "deploy.ps1 CLAP bundle" deploy.ps1 "\$clapName = \"$DISPLAY.clap\""
printf '\nstale spellings\n'
# File list for the scan below. Prefer git, so build output and any vendored
# tree stay out of it. Fall back to find when this is not a checkout - an
# exported tarball, or a CI step that fetched without git metadata - because
# `git ls-files` there returns nothing, and an empty list would report every
# stale spelling as absent. A check that passes hardest when it can see least
# is worse than no check.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
file_list() { git ls-files; }
scan_source="git"
else
file_list() { find . -path ./.git -prune -o -path ./target -prune -o -type f -print | sed 's|^\./||'; }
scan_source="find (not a git checkout)"
fi
info "scanning tracked files via $scan_source"
# Every canonical spelling concatenated, for the substring test below.
CANONICAL_BLOB="$PKG
$DESCRIPTION
$CLAP_DESCRIPTION
$STRUCT
$PARAMS_STRUCT
$DISPLAY
$PLUGIN_NAME
$VENDOR
$EMAIL
$HOMEPAGE
$CLAP_ID
$VST3_CLASS_ID"
# Fed by here-doc rather than a pipe on purpose: a piped `while` runs in a
# subshell, and the pass/fail counters it increments would be discarded.
while IFS= read -r s; do
[ -n "$s" ] || continue
# Still part of a canonical value? Then it is current branding, not a leftover.
# This is what lets the list ship pre-seeded: entries arm themselves as you
# rename. Substring, not equality, because placeholders nest - "your-domain"
# lives inside the canonical CLAP_ID, and "YOUR-USERNAME" inside the homepage.
# Over-skipping is the safe direction: a false FAIL on a name you still use
# would train you to ignore this section.
case "$CANONICAL_BLOB" in
*"$s"*) note "\"$s\" (still canonical)"; continue ;;
esac
# This script names every placeholder by definition, so it can never be a hit.
hits=$(file_list 2>/dev/null | while read -r f; do
[ "$f" = "check-branding.sh" ] || { [ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null; }
done)
if [ -z "$hits" ]; then ok "no occurrences of \"$s\""
else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi
done <<STALE_LIST
$STALE
STALE_LIST
# Template scaffolding, not drift: TODOs are legitimate while you are still
# building. Reported so a finished plugin does not quietly ship with them.
# `grep -c` prints 0 AND exits 1 when there are no matches, so `|| echo 0`
# would append a second zero and make the count unparseable. Swallow the
# status instead, and default an empty result (missing file) to zero.
todos=$(grep -c 'TODO' src/lib.rs 2>/dev/null || :)
[ -n "$todos" ] || todos=0
printf '\nscaffolding\n'
if [ "$todos" -eq 0 ]; then ok "no TODO markers left in src/lib.rs"
else info "$todos TODO marker(s) still in src/lib.rs (not a failure)"; fi
printf '\n%d passed, %d failed' "$pass" "$fail"
[ "$skip" -gt 0 ] && printf ', %d skipped' "$skip"
printf '\n'
[ "$fail" -eq 0 ] || exit 1
+54 -44
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
use nih_plug::prelude::*;
use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState};
use std::sync::Arc;
// ──────────────────────────────────────────────
@@ -8,31 +9,36 @@ use std::sync::Arc;
// See musiikki/codename-206/src/params.rs for a real example.
// ──────────────────────────────────────────────
struct PluginParams;
#[derive(Params)]
struct PluginParams {
/// Editor window size, persisted with the plugin state.
#[persist = "editor-state"]
editor_state: Arc<EguiState>,
impl Params for PluginParams {
fn as_slice(&self) -> &[AtomicParam] {
&[]
/// Example parameter — replace with your own. The `id` is the stable key the host
/// stores automation against, so never change it once you've shipped.
#[id = "gain"]
gain: FloatParam,
}
fn as_mut_slice(&mut self) -> &mut [AtomicParam] {
&mut []
impl Default for PluginParams {
fn default() -> Self {
Self {
editor_state: EguiState::from_size(320, 160),
gain: FloatParam::new(
"Gain",
util::db_to_gain(0.0),
FloatRange::Skewed {
min: util::db_to_gain(-30.0),
max: util::db_to_gain(30.0),
factor: FloatRange::gain_skew_factor(-30.0, 30.0),
},
)
.with_smoother(SmoothingStyle::Logarithmic(50.0))
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
}
fn param_by_id(&self, _id: ParamId) -> Option<&AtomicParam> {
None
}
fn param_by_id_mut(&mut self, _id: ParamId) -> Option<&mut AtomicParam> {
None
}
fn param_by_name(&self, _name: &str) -> Option<&AtomicParam> {
None
}
fn param_by_name_mut(&mut self, _name: &str) -> Option<&mut AtomicParam> {
None
}
}
@@ -40,18 +46,11 @@ impl Params for PluginParams {
// Plugin
// ──────────────────────────────────────────────
#[derive(Default)]
struct MyPlugin {
params: Arc<PluginParams>,
}
impl Default for MyPlugin {
fn default() -> Self {
Self {
params: Arc::new(PluginParams),
}
}
}
impl Plugin for MyPlugin {
// TODO: Replace these placeholder values with your plugin's metadata.
const NAME: &'static str = "Plugin Name";
@@ -61,20 +60,18 @@ impl Plugin for MyPlugin {
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
AudioIOLayout {
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
main_input_channels: NonZeroU32::new(2),
main_output_channels: NonZeroU32::new(2),
..AudioIOLayout::const_default()
},
];
}];
const MIDI_INPUT: MidiConfig = MidiConfig::None;
const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
type SysSysexMessage = ();
type SysExMessage = ();
type BackgroundTask = ();
fn params(&self) -> Arc<dyn Params> {
@@ -82,9 +79,19 @@ impl Plugin for MyPlugin {
}
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
// TODO: Implement your egui editor here.
// return Some(Box::new(MyEditor::new(self.params.clone())));
None
// TODO: Build your own egui layout here.
let params = self.params.clone();
create_egui_editor(
self.params.editor_state.clone(),
(),
|_, _| {},
move |egui_ctx, setter, _state| {
egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.label("Gain");
ui.add(widgets::ParamSlider::for_param(&params.gain, setter));
});
},
)
}
fn initialize(
@@ -104,19 +111,21 @@ impl Plugin for MyPlugin {
_aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
// TODO: Implement your DSP here.
// This is a passthrough — replace with your audio processing.
// TODO: Implement your DSP here. This applies the example gain parameter; note that
// the smoother is advanced once per frame so automation doesn't zipper.
for mut frame in buffer.iter_samples() {
for ch in 0..frame.len() {
*frame.get_mut(ch).unwrap() = *frame.get(ch).unwrap();
let gain = self.params.gain.smoothed.next();
for sample in frame.iter_mut() {
*sample *= gain;
}
}
ProcessStatus::Normal
}
}
impl ClapPlugin for MyPlugin {
// TODO: Replace with a unique CLAP ID, e.g. "com.your-domain.your-plugin-name"
// TODO: Replace with a unique CLAP ID in reverse-DNS form, e.g. "com.acme.fuzzbox"
const CLAP_ID: &'static str = "com.your-domain.plugin-name";
const CLAP_DESCRIPTION: Option<&'static str> = Some("Your plugin description");
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
@@ -124,13 +133,14 @@ impl ClapPlugin for MyPlugin {
const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect];
}
#[cfg(feature = "vst3")]
impl Vst3Plugin for MyPlugin {
// TODO: Replace with a unique 16-byte Class ID.
// Must be exactly 16 bytes — padded with '!' if shorter.
const VST3_CLASS_ID: [u8; 16] = *b"Unique16CharId!!";
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx];
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[Vst3SubCategory::Fx];
}
nih_export_clap!(MyPlugin);
#[cfg(feature = "vst3")]
nih_export_vst3!(MyPlugin);