Compare commits

...

33 Commits

Author SHA1 Message Date
Mikkeli Matlock ce410b0047 Merge branch 'fix/gitattributes-stray-cr' 2026-09-14 01:41:39 +09:00
Mikkeli Matlock 5212b7806b fix: remove a stray CR byte from .gitattributes
The previous commit wrote a literal carriage-return byte into a comment, where
the text was meant to show the two characters backslash-r. Git's content sniffing
treats a lone CR as a sign of binary data, so .gitattributes itself was classed
-text (git ls-files --eol reported i/-text): the one file in the template that
the new LF policy could not normalise, and one every repo created from it would
have inherited.

The rules themselves were never affected - the byte sat inside a comment.

The comment now says "a trailing CR byte" in words. .gitattributes reports
i/lf w/lf, and `git add --renormalize .` stages nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-09-14 01:41:39 +09:00
Mikkeli Matlock 09d98bef06 Merge branch 'chore/lf-line-endings' 2026-09-14 01:39:17 +09:00
Mikkeli Matlock 3abbb06073 chore: pin LF line endings in .gitattributes
The branding toolkit reads tools/branding-sites.tsv line by line with `read`.
On Windows, Git for Windows' default core.autocrlf=true checks that file out as
CRLF, so every row's last field - the pattern - gains a trailing \r and matches
nothing. In a child repo this made check-branding.sh report all 29 of its sites
as missing on Windows while the same commit passed on the Linux CI runner, and
brand-plugin.sh uses that same field as its sed search pattern. Only *.sh was
pinned to LF, which protected the scripts but not their data.

This is the template, so the fix belongs here: every repo created from it
inherits the policy instead of rediscovering the bug.

    * text=auto eol=lf       LF in the repo and every working tree, every OS
    *.sh text eol=lf         kept explicit: a CRLF shebang is a hard failure
    *.bat/*.cmd eol=crlf     the one exception; cmd.exe misparses LF batch files

An attribute beats core.autocrlf, so a fresh clone gets LF however its machine
is configured. Verified in a throwaway repo under autocrlf=true.

No blob changes: every committed blob was already LF, and
`git add --renormalize .` stages nothing but this file. Working tree
re-checked-out to match (14 files CRLF -> LF). Checker 38 passed / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-09-14 01:39:17 +09:00
mikkeli 8630ba0c5b Merge branch 'ci/validate-workflow-changes'
CI / Build (push) Successful in 34s
Include the workflow file in both path filters so CI changes are self-validating.
2026-09-04 19:33:12 +09:00
mikkeli fad8e21b2b ci: let a workflow change validate itself
Adds .github/workflows/** to both path filters. A broken step, a moved
script path or a wrong runner label now fails on the commit that caused
it, instead of surfacing later against an unrelated source change and
looking like that change's fault.

This session produced exactly that bug: after the branding tools moved
under tools/, the workflow still ran `sh check-branding.sh` at the old
path, and it was caught by reading the file rather than by CI.

The limit is worth stating: this validates the job body, not the trigger
block. Break `on:` and the symptom is silence, which is indistinguishable
from success.

Workflow edits are rare enough that the extra builds cost nothing, and CI
config is the definition of how the project is built and verified — it
belongs on the source side of "source code edits only", not with the docs.

kolmekivi additionally gains the pull_request path filter the other two
already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-04 19:33:12 +09:00
mikkeli f8aa3ee888 Merge branch 'ci/tighten-triggers'
Apply the same source-path filter to pull_request that push already had.
2026-09-04 19:26:35 +09:00
mikkeli 24c6ee0655 ci: path-filter the pull_request trigger too
The push trigger was already restricted to master and to source paths,
but pull_request carried only a branch filter — so a PR touching nothing
but the README would have run a full build. A branch filter constrains
where a workflow fires, not what fires it.

Both filters now list the same six paths. A docs-only change runs nothing,
on either trigger.

Note that .github/workflows/** is deliberately absent from both lists, so
editing this file does not run it. That is the intended trade: fewer
pointless builds, at the cost of a broken workflow surfacing on the next
source change rather than immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-04 19:26:35 +09:00
mikkeli de2dbbc218 Merge branch 'chore/homepage'
CI / Build (push) Successful in 31s
Point the placeholder homepage at the org wiki rather than a nonexistent GitHub URL.
2026-09-04 16:14:32 +09:00
mikkeli 1dc836dca5 chore: point the placeholder homepage at the org wiki
The Cargo homepage feeds Plugin::URL, which hosts can show to users, so
the placeholder should be somewhere reachable rather than a github.com URL
that never existed.

This retires YOUR-USERNAME and YOUR-PLUGIN-NAME as canonical values, which
is the self-arming STALE mechanism working as designed: both were skipped
while they were part of HOMEPAGE and became live checks the moment they
were not. Both pass, since the whole URL was replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-04 16:14:32 +09:00
mikkeli 7b7ba69082 Merge branch 'chore/deny-future-incompat'
CI / Build (push) Successful in 28s
Inherit the future-incompatible guard in every plugin built from this template.
2026-09-03 08:25:56 +09:00
mikkeli 689891d01b chore: deny future-incompatible warnings
Carried back from Bari 206, where sixteen deferred hard errors had
accumulated unnoticed because nothing distinguished them from style
warnings in the build log.

Future-incompatibility warnings are rustc saying this code stops compiling
on some later release. Denying the group makes that a build failure the
day it appears rather than a surprise during an unrelated toolchain bump.
Every plugin cloned from this template inherits the guard.

Clippy and style lints stay warnings; only the future_incompatible group
is denied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-03 08:25:56 +09:00
mikkeli a09b28d5fa Merge branch 'fix/narrow-patterns'
CI / Build (push) Successful in 26s
Match the narrowest distinctive fragment so rustfmt line-wrapping cannot break a
site check, and leave STALE alone when nothing was retired.
2026-09-02 16:37:59 +09:00
mikkeli 62ac2e9d6c fix: match the narrowest distinctive fragment, and stop padding STALE
Integrating into Bari 206 turned up a pattern that cannot match correctly
formatted source. Its CLAP_DESCRIPTION is long enough that rustfmt wraps
the assignment:

    const CLAP_DESCRIPTION: Option<&'static str> =
        Some("Multiband compressor/limiter (...)");

A site pattern spanning `const ... = Some("...")` never matches that, and
the fix is general rather than specific to this field: a site pattern
should be the narrowest fragment that is still distinctive, so formatting
decisions above it cannot break the check. Some("...") is unique enough
and lives on one line whatever rustfmt does with the rest.

Also stops appending a bare newline to STALE when nothing was retired,
which a no-op regeneration does on every run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-02 16:37:59 +09:00
mikkeli 4a7dad95b3 Merge branch 'feat/author-field'
Separate cargo AUTHOR from Plugin::VENDOR and rewrite known sites from the table
before falling back to value substitution.
2026-09-02 16:36:34 +09:00
mikkeli 454e80bcd5 fix: separate cargo AUTHOR from Plugin::VENDOR, and rewrite sites precisely
Integrating this toolkit into Bari 206 surfaced two things the template
could not express, both of which would have produced wrong output.

**AUTHOR.** Cargo's authors entry names whoever wrote the plugin; VENDOR
is the organisation that ships it. Bari 206 separates them - "Mikkeli
Matlock" authored it, "Novoyuuparosk" vends it - while this template only
happened to work because its two placeholders were identical. AUTHOR is
now its own value defaulting to VENDOR, pinned in branding.conf only when
it actually differs so the file keeps showing that it derives.

**Site-directed rewriting.** That default is exactly what broke the
rename: with AUTHOR defaulting to VENDOR, both fields hold the same old
string, and a global value substitution cannot tell which occurrence
belongs to which. Renaming the vendor put the org name in the authors
field.

The site table already holds the old resolved literal for every site it
owns, and regeneration produces the new one, so rewriting by that pair is
unambiguous. brand-plugin.sh now does that first and falls back to value
substitution for everything the table does not enumerate - doc comments,
use sites in other modules, prose. The order matters and is load-bearing:
run the global pass first and it consumes the old literals, leaving the
precise pass nothing to match.

Verified on the ambiguous case: renaming with a distinct author now puts
the person in Cargo.toml's authors and the org in Plugin::VENDOR, where
the previous implementation put the org in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-02 16:36:34 +09:00
mikkeli 0500de9eb9 ci: widen the path filter to tools/
CI / Build (push) Successful in 30s
The filter still named check-branding.sh at the repo root, a path that no
longer exists after the move — so a change to the branding toolkit would
not have triggered CI at all. Silent, and exactly the failure the toolkit
exists to prevent elsewhere.

'tools/**' also covers the deploy scripts that will land there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-01 10:56:22 +09:00
mikkeli 5f612dde57 Merge branch 'refactor/tools-dir'
Move the branding toolkit into tools/, derive names from PLUGIN_NAME, and drive
the checker from a data table so the scripts are identical across plugins.
2026-09-01 10:15:34 +09:00
mikkeli 8c24b83ea3 refactor: move branding into tools/ and drive the checker from a table
Three changes that only make sense together.

**tools/**. check-branding.sh, brand-plugin.sh and branding.conf were
accumulating in the repo root, and a deploy.sh/bat/ps1 set would land
there too. They now live in tools/ and resolve the repo root explicitly
rather than assuming their own directory is it, so they behave the same
however they are invoked.

**One name drives the rest.** branding.conf now derives the package name,
the Rust types and the display name from PLUGIN_NAME, each overridable.
Derivation-by-default removes a class of drift; the override matters
because the derivation is sometimes invalid — "206 Prototype" derives to
"206_prototype", which cargo rejects outright — and because PLUGIN_NAME is
cosmetic while the package name is structural. A display tweak should not
silently rename the crate, its bundle lookup, the CI command and the
lockfile. This template overrides three of them, which a real plugin
normally will not: its names are the template's own rather than derived
from a product name.

An overridden PKG that no longer matches the derivation is reported at
info level, never failed. The override is usually deliberate; a display
rename that forgot the package looks identical from here, and is not.

**The site table is data.** branding-sites.tsv lists every place a name
must appear and in what form; check-branding.sh iterates it instead of
carrying 29 check calls in shell. The scripts are now identical across
every plugin built from this template, with only the table and conf
differing — which matters because the template and codename-206 had
already diverged by two checks, so every sync was a shell merge.

brand-plugin.sh regenerates the rows it owns and preserves the rest, so a
hand-added row for a file this template does not ship survives a rename.
Custom rows are value-substituted like any other tracked file, so they
track the rename rather than going stale — the first cut excluded the
whole table from substitution and got that wrong.

brand-plugin.sh also gained an interactive mode: run it with no arguments
and it prompts for each value with the current one as the default, so
Enter keeps it and a run that answers nothing changes nothing. It only
engages when there is a terminal to ask at, so CI gets the usage message
and exit 2 rather than a hang.

Verified by applying a full rename to a sandboxed copy: 8 files rewritten,
29 owned rows regenerated, 1 custom row preserved and substituted, checker
green at 48/0/7, and the result compiles as bari_206.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-01 10:15:34 +09:00
mikkeli 2dd47b5afd Merge branch 'feat/branding-toolkit'
Split branding into check-branding.sh and brand-plugin.sh over a shared
branding.conf, and give the checker positive assertions as well as absence.
2026-08-31 23:11:47 +09:00
mikkeli a46e7eb961 feat: split branding into a checker and a helper over one canonical file
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
2026-08-31 23:11:47 +09:00
mikkeli b05ad27a24 Merge branch 'ci/build-gate'
CI / Build (push) Successful in 27s
Add the minimal CI gate: branding check, both feature sets built, test, bundle.
2026-08-31 22:27:35 +09:00
mikkeli 054deea900 ci: gate pushes on a build of both feature sets
Adds the minimal check a plugin repo actually needs: does it still build,
and do the identity strings still agree.

Both feature sets are built rather than just the default, because the
`vst3` cfg gates are exactly the kind of thing that rots — a default build
never compiles the Vst3Plugin impl, so a break there would otherwise
surface only when someone tried to ship a VST3. The bundle step is
included for the same reason: `cargo build` never exercises the xtask
bundler, and bundler.toml drifting from the crate name is invisible until
you try to produce an artifact.

The branding check runs first because it needs no toolchain and finishes
in under a second, so a half-landed rename fails before a build is spent
on it.

Performance testing is deliberately absent. Realtime cost is only
meaningful against an 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, so no external crate
can implement the trait. A static lint for locks in the audio path was
considered and dropped — `lock` and `borrow` are ordinary identifiers, not
reserved words, so a substring search matches every occurrence of "block"
in DSP code while missing `println!`, `recv()` and `Condvar::wait()`,
which are the likelier offenders. `assert_process_allocs` remains the real
guarantee, and it only fires in a debug build inside a host.

Verified by running the exact step sequence as the gitea-runner user under
the service's environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-31 22:27:35 +09:00
mikkeli 1dc45dc235 Merge branch 'chore/relicense-gpl3'
Relicense to GPL-3.0-or-later to match the plugins built from this template,
and credit the ISC-licensed NIH-plug upstream the skeleton derives from.
2026-08-31 22:01:51 +09:00
mikkeli 9c0e31d783 chore: relicense to GPL-3.0-or-later and credit the ISC upstream
Apache-2.0 was the wrong declaration for two independent reasons.

Enabling the `vst3` feature links vst3-sys, which is GPLv3-or-later by its
own authors' choice — they describe their port as a derivative of
Steinberg's SDK that "was not made in isolation", so the obligation holds
regardless of any question about Steinberg's own rights. Gating the
feature kept the default build clean, but plugins built from this template
are intended to ship VST3, so GPL is where they land anyway. Declaring it
up front is more honest than deferring the decision to every clone.

Separately, parts of src/lib.rs derive from Robbert van der Helm's
NIH-plug cookiecutter template and its `gain` example, both ISC. ISC's one
condition is that its copyright and permission notice travel with all
copies, and this repo carried neither — a compliance gap that existed
regardless of the GPL question. ISC is GPL-compatible, so the notice is
retained in a new NOTICE file rather than superseded.

The `vst3` feature stays opt-in, but its rationale changes: it is no
longer about licence compliance, only about not pulling a sizeable
dependency into a CLAP-only build. The Cargo.toml comment said otherwise
and would have misled the next reader.

Also corrects the README's claim that you must "accept Steinberg's VST3
SDK licence". No SDK source is vendored or redistributed here, so there is
nothing to accept at build time. The real constraint is vst3-sys's GPLv3,
plus Steinberg's separate trademark in "VST" — a different regime from
copyright that no source licence resolves.

Copyright in the Irene <irene@novoyuuparosk.org> commits is held by the
repository owner (an agent identity), so no third-party agreement was
needed for this relicense.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-31 22:00:33 +09:00
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
15 changed files with 4218 additions and 121 deletions
+20
View File
@@ -0,0 +1,20 @@
# Line endings: LF in the repository and in every working tree, on every OS.
#
# This file is the policy, not each machine's git config. Git for Windows installs
# with core.autocrlf=true, which checks text out as CRLF; an attribute here beats
# that setting, so a fresh clone gets LF regardless of how its machine is set up.
#
# It matters beyond tidiness. tools/branding-sites.tsv is read line by line with
# `read`, and under CRLF every row's last field gained a trailing CR byte - so the
# branding checker reported all 29 sites as missing on Windows while passing on the
# Linux CI runner. text=auto leaves genuinely binary files alone.
* text=auto eol=lf
# Shell scripts are pinned explicitly as well: a strict POSIX sh (dash, CI
# containers) rejects a CRLF shebang outright rather than misbehaving quietly.
*.sh text eol=lf
# The exception. cmd.exe misparses LF-only batch files around labels and long
# lines, so these alone are checked out CRLF. The repository copy is still LF.
*.bat text eol=crlf
*.cmd text eol=crlf
+77
View File
@@ -0,0 +1,77 @@
name: CI
on:
push:
branches: [ master ]
paths:
- 'src/**'
- 'xtask/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'bundler.toml'
- 'tools/**'
# This workflow validates itself: a broken step, a moved script path or a
# wrong runner label then fails loudly on the commit that caused it, rather
# than surfacing later against an unrelated source change. Note the limit -
# it checks the job body, not the trigger block above. Break `on:` and the
# symptom is silence, which looks like success.
- '.github/workflows/**'
# Mirrors the push filter. Without it a PR touching only the README would run a
# full build - the branch filter alone constrains where, not what.
pull_request:
branches: [ master ]
paths:
- 'src/**'
- 'xtask/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'bundler.toml'
- 'tools/**'
- '.github/workflows/**'
workflow_dispatch:
# Target the self-hosted Rust/audio runner registered to the `musiikki` org.
#
# The runner is host-mode, so the rustup toolchain installed for the
# `gitea-runner` user is used directly — nothing installs per run, and the cargo
# registry cache stays warm between jobs.
#
# NOTE: `rust-audio` is an ORG-SCOPED label. A plugin cloned from this template
# into a different org will not see that runner, and its jobs will queue
# indefinitely rather than fail. Either register a runner advertising this label
# for that org, or change `runs-on` below.
jobs:
build:
name: Build
runs-on: rust-audio
steps:
- uses: actions/checkout@v4
- name: Toolchain
run: |
cargo --version
rustc --version
echo "CARGO_HOME=${CARGO_HOME:?not set on this runner}"
# Cheap and needs no toolchain, so it runs first: a rename that only
# landed in half the files is worth catching before spending a build on it.
- name: Branding consistency
run: sh tools/check-branding.sh
# The default build is CLAP-only; the vst3 feature is opt-in, so both
# configurations have to be built to know the cfg gates are still correct.
- name: Build (default, CLAP only)
run: cargo build --release
- name: Build (--features vst3)
run: cargo build --release --features vst3
- name: Test
run: cargo test --release --features vst3
# Proves the xtask bundler still works, which a plain `cargo build` does
# not exercise — the bundle step is where bundler.toml and the crate name
# have to agree.
- name: Bundle
run: cargo xtask bundle plugin_name --release --features vst3
Generated
+2534
View File
File diff suppressed because it is too large Load Diff
+22 -4
View File
@@ -1,23 +1,41 @@
[package]
name = "nih_plug_template"
name = "plugin_name"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your@email.com>"]
homepage = "https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME"
homepage = "https://wiki.novoyuuparosk.org"
description = "A minimal NIH-plug audio plugin"
license = "Apache-2.0"
license = "GPL-3.0-or-later"
[workspace]
members = ["xtask"]
[features]
# CLAP only by default. This crate is GPL-3.0-or-later, so enabling VST3 no
# longer raises a licensing question — the gate now just keeps the default build
# light, since `vst3-sys` and its macro machinery are a sizeable dependency a
# CLAP-only plugin never needs. Flip `default` to ["vst3"] if you always want it:
# cargo xtask bundle plugin_name --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"]
# Future-incompatibility warnings are deferred hard errors: rustc is telling you
# this code stops compiling on some later release. Denying the group turns that
# into a build failure the day it appears, rather than accumulating unnoticed
# until a toolchain bump breaks the build on a day you did not choose. Style and
# clippy lints are deliberately untouched - they stay warnings.
[lints.rust]
future_incompatible = { level = "deny", priority = -1 }
[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]
+642 -41
View File
@@ -1,73 +1,674 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
1. Definitions.
Preamble
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
The precise terms and conditions for copying, distribution and
modification follow.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
TERMS AND CONDITIONS
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
0. Definitions.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
"This License" refers to version 3 of the GNU General Public License.
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
A "covered work" means either the unmodified Program or a work based
on the Program.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
1. Source Code.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
END OF TERMS AND CONDITIONS
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
APPENDIX: How to apply the Apache License to your work.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
Copyright 2026 musiikki
The Corresponding Source for a work in source code form is that
same work.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
2. Basic Permissions.
http://www.apache.org/licenses/LICENSE-2.0
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2026 Mikkeli Matlock
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) 2026 Mikkeli Matlock
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+46
View File
@@ -0,0 +1,46 @@
Plugin Name
Copyright 2026 Mikkeli Matlock
This product is licensed under the GNU General Public License v3.0 or later.
See LICENSE for the full terms.
--------------------------------------------------------------------------------
Third-party material
--------------------------------------------------------------------------------
Portions of src/lib.rs derive from the NIH-plug cookiecutter template and from
NIH-plug's `gain` example plugin, both by Robbert van der Helm:
https://github.com/robbert-vdh/nih-plug-template
https://github.com/robbert-vdh/nih-plug
Those portions are licensed under the ISC License:
ISC License
Copyright (c) 2022 Robbert van der Helm
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ISC is GPL-compatible, so this notice is retained rather than superseded.
--------------------------------------------------------------------------------
Note on the `vst3` feature
--------------------------------------------------------------------------------
Building with `--features vst3` links `vst3-sys`, which is licensed GPLv3 or
later. This is the reason a distributed VST3 build must be GPL-licensed. No
Steinberg SDK source is vendored or redistributed by this project; "VST" is a
trademark of Steinberg Media Technologies GmbH, and using it to describe a
distributed product is governed by Steinberg's trademark terms, separately from
any copyright licence above.
+119 -16
View File
@@ -13,24 +13,91 @@ 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.
Everything for it lives in `tools/`:
```
tools/branding.conf the names, and what derives from what
tools/branding-sites.tsv where each name has to appear, and in what form
tools/brand-plugin.sh changes names
tools/check-branding.sh verifies them
```
```bash
sh tools/brand-plugin.sh # interactive; Enter keeps a value
sh tools/brand-plugin.sh --display "Fuzz Box" # or non-interactive; dry run
sh tools/brand-plugin.sh --display "Fuzz Box" --apply
sh tools/check-branding.sh # exit 0 = consistent, 1 = drift
```
**One name drives the rest.** `PLUGIN_NAME` derives the package name (`Fuzz Box` →
`fuzz_box`), the Rust types (`FuzzBox`, `FuzzBoxParams`) and the display name. Each is
overridable in `branding.conf`, because the derivation is sometimes wrong and sometimes
outright invalid — `206 Prototype` derives to `206_prototype`, which Cargo rejects since a
package name cannot start with a digit. The helper refuses those, along with Rust keywords
and built-in crate names.
**The checker does both halves.** Every canonical value is asserted *present* where it
belongs, and every retired spelling is swept for. Absence alone is not enough: renaming to
the wrong new spelling would pass a stale-sweep and still break CI.
**Two things it will not do for you.** `CLAP_ID` and `VST3_CLASS_ID` are how hosts recognise
saved projects and presets — change either and every existing session silently loses the
plugin, with no error. They need `--allow-identity-change`, and the 16-byte class ID is
*proposed* rather than derived, because that choice is yours. And `CLAP_DESCRIPTION` is
rejected if it carries a retired name or the repo codename: hosts show it to users who never
knew your development history.
**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` records it so it
can be kept out of user-facing text.
Adding a site the template doesn't ship is one row in `branding-sites.tsv` with owner
`custom` — no shell to edit, and it survives regeneration. Rows the helper owns are rewritten
on each rename; custom rows are carried through and value-substituted like any other file.
Both files can be hand-edited; the helper is what keeps them agreeing, so run the checker
after if you do.
## 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
cargo xtask bundle nih_plug_template --release
# Build + bundle the CLAP (default)
cargo xtask bundle plugin_name --release
# Add the VST3 as well — see the licensing note below before you do this
cargo xtask bundle plugin_name --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,33 +108,69 @@ 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
tools/ # Branding toolkit — see Renaming
.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
Apache-2.0
**GPL-3.0-or-later.** See `LICENSE` for the terms and `NOTICE` for third-party attribution.
## VST3 Licensing Note
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.
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.
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.
+2 -2
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]
[plugin_name]
name = "Plugin Name"
+64 -54
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-or-later
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 PluginNameParams {
/// 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,
}
impl Default for PluginNameParams {
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 as_mut_slice(&mut self) -> &mut [AtomicParam] {
&mut []
}
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,19 +46,12 @@ impl Params for PluginParams {
// Plugin
// ──────────────────────────────────────────────
struct MyPlugin {
params: Arc<PluginParams>,
#[derive(Default)]
struct PluginName {
params: Arc<PluginNameParams>,
}
impl Default for MyPlugin {
fn default() -> Self {
Self {
params: Arc::new(PluginParams),
}
}
}
impl Plugin for MyPlugin {
impl Plugin for PluginName {
// TODO: Replace these placeholder values with your plugin's metadata.
const NAME: &'static str = "Plugin Name";
const VENDOR: &'static str = "Your 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"
impl ClapPlugin for PluginName {
// 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];
}
impl Vst3Plugin for MyPlugin {
#[cfg(feature = "vst3")]
impl Vst3Plugin for PluginName {
// 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);
nih_export_vst3!(MyPlugin);
nih_export_clap!(PluginName);
#[cfg(feature = "vst3")]
nih_export_vst3!(PluginName);
+355
View File
@@ -0,0 +1,355 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# brand-plugin.sh - rename this plugin, everywhere, in one go.
#
# check-branding.sh tells you what disagrees. This changes it. They share
# branding.conf: this script rewrites that file and every copy of the old values
# in tracked files, then runs the checker and reports what it says. A run that
# ends with the checker green is a rename that landed.
#
# Dry run by default:
# sh brand-plugin.sh --display "Bari 206"
# sh brand-plugin.sh --display "Bari 206" --vendor Novoyuuparosk --apply
#
# Options:
# --display NAME the product name, with spaces and capitals. Everything
# else that can be derived, is derived from this.
# --pkg NAME override the derived Cargo package name.
# --vendor NAME --email ADDR --homepage URL
# --description D --clap-description D
# --struct NAME override the derived Rust type name.
# --allow-identity-change permit CLAP_ID / VST3_CLASS_ID to move. See below.
# --apply actually write. Without it, nothing is modified.
set -u
# Resolve the repo root: these live in tools/, every path they touch is
# repo-relative, and they must behave the same wherever they are invoked from.
cd "$(dirname "$0")/.." || exit 1
[ -f tools/branding.conf ] || { printf 'tools/branding.conf not found\n' >&2; exit 1; }
# shellcheck disable=SC1091
. ./tools/branding.conf
OLD_PKG="$PKG"; OLD_DISPLAY="$DISPLAY"; OLD_PLUGIN_NAME="$PLUGIN_NAME"
OLD_VENDOR="$VENDOR"; OLD_EMAIL="$EMAIL"; OLD_HOMEPAGE="$HOMEPAGE"; OLD_AUTHOR="$AUTHOR"
OLD_DESCRIPTION="$DESCRIPTION"; OLD_CLAP_DESCRIPTION="$CLAP_DESCRIPTION"
OLD_CLAP_ID="$CLAP_ID"; OLD_VST3="$VST3_CLASS_ID"
OLD_STRUCT="$STRUCT"; OLD_PARAMS_STRUCT="$PARAMS_STRUCT"
new_display=""; new_pkg=""; new_vendor=""; new_email=""; new_homepage=""
new_description=""; new_clap_description=""; new_struct=""; new_author=""
new_clap_id=""; new_vst3=""; apply=0; allow_identity=0
while [ $# -gt 0 ]; do
case "$1" in
--display) new_display="${2:?}"; shift 2 ;;
--pkg) new_pkg="${2:?}"; shift 2 ;;
--vendor) new_vendor="${2:?}"; shift 2 ;;
--author) new_author="${2:?}"; shift 2 ;;
--email) new_email="${2:?}"; shift 2 ;;
--homepage) new_homepage="${2:?}"; shift 2 ;;
--description) new_description="${2:?}"; shift 2 ;;
--clap-description) new_clap_description="${2:?}"; shift 2 ;;
--struct) new_struct="${2:?}"; shift 2 ;;
--clap-id) new_clap_id="${2:?}"; shift 2 ;;
--vst3-class-id) new_vst3="${2:?}"; shift 2 ;;
--allow-identity-change) allow_identity=1; shift ;;
--apply) apply=1; shift ;;
-h|--help) sed -n '3,25p' "$0"; exit 0 ;;
*) printf 'unknown option: %s\n' "$1" >&2; exit 2 ;;
esac
done
# Interactive when nothing was passed and there is a terminal to ask at. Every
# prompt offers the current value; Enter keeps it, so a run that answers nothing
# is a no-op rather than a surprise. Optional fields additionally accept "-" to
# clear them.
ask() { # ask VARNAME PROMPT CURRENT
printf ' %s [%s]: ' "$2" "$3" > /dev/tty
IFS= read -r _reply < /dev/tty || _reply=""
[ -n "$_reply" ] || _reply="$3"
[ "$_reply" = "-" ] && _reply=""
eval "$1=\$_reply"
}
if [ -z "$new_display" ] && [ -t 0 ] && [ -c /dev/tty ]; then
printf 'branding this plugin - Enter keeps the current value, "-" clears an optional one\n\n'
ask new_display "display name " "$OLD_DISPLAY"
ask new_vendor "vendor (org) " "$OLD_VENDOR"
ask new_author "cargo author " "$OLD_AUTHOR"
ask new_email "email " "$OLD_EMAIL"
ask new_homepage "homepage " "$OLD_HOMEPAGE"
ask new_description "cargo description" "$OLD_DESCRIPTION"
ask new_clap_description "clap description" "$OLD_CLAP_DESCRIPTION"
printf '\n'
fi
[ -n "$new_display" ] || { printf 'nothing to do: pass --display "New Name" (or run interactively)\n' >&2; exit 2; }
# --- derivation -------------------------------------------------------------
# Package name: lowercase, every run of non-alphanumerics becomes one separator,
# ends trimmed. Cargo accepts both hyphens and underscores in a package name and
# converts to underscores for the built artifact, which nih_plug_xtask already
# accounts for; underscores are used here to match the Rust convention.
derive_pkg() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -e 's/[^a-z0-9]\{1,\}/_/g' -e 's/^_//' -e 's/_$//'
}
# Rust type name: CamelCase, non-alphanumerics dropped entirely.
derive_struct() {
printf '%s' "$1" | sed -e 's/[^A-Za-z0-9]\{1,\}/ /g' \
| awk '{for(i=1;i<=NF;i++) printf "%s%s", toupper(substr($i,1,1)), substr($i,2); print ""}'
}
[ -n "$new_pkg" ] || new_pkg=$(derive_pkg "$new_display")
[ -n "$new_struct" ] || new_struct=$(derive_struct "$new_display")
new_params_struct="${new_struct}Params"
[ -n "$new_vendor" ] || new_vendor="$OLD_VENDOR"
[ -n "$new_author" ] || new_author="$OLD_AUTHOR"
[ -n "$new_email" ] || new_email="$OLD_EMAIL"
[ -n "$new_homepage" ] || new_homepage="$OLD_HOMEPAGE"
[ -n "$new_description" ] || new_description="$OLD_DESCRIPTION"
[ -n "$new_clap_description" ] || new_clap_description="$OLD_CLAP_DESCRIPTION"
# --- validation -------------------------------------------------------------
fail=0
die() { printf ' INVALID %s\n' "$1"; fail=$((fail+1)); }
case "$new_pkg" in
"") die "derived package name is empty" ;;
[0-9]*) die "package name \"$new_pkg\" starts with a digit - cargo rejects this outright" ;;
esac
case "$new_pkg" in
test|core|std|alloc|proc_macro)
die "package name \"$new_pkg\" collides with a built-in crate" ;;
as|break|const|continue|crate|dyn|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|static|struct|super|trait|true|type|unsafe|use|where|while|async|await|box|do|final|macro|override|priv|typeof|unsized|virtual|yield|try)
die "package name \"$new_pkg\" is a Rust keyword - cargo rejects this" ;;
esac
# bundler.toml uses the package name as a TOML bare key.
case "$new_pkg" in *[!a-zA-Z0-9_-]*) die "package name \"$new_pkg\" is not a valid TOML bare key" ;; esac
case "$new_struct" in ""|[0-9]*) die "derived type name \"$new_struct\" is not a valid Rust identifier" ;; esac
# --- frozen identity --------------------------------------------------------
identity_note=""
if [ -n "$new_clap_id" ] || [ -n "$new_vst3" ]; then
if [ "$allow_identity" -eq 0 ]; then
die "CLAP_ID / VST3_CLASS_ID are frozen identity; pass --allow-identity-change to move them"
fi
fi
[ -n "$new_clap_id" ] || new_clap_id="$OLD_CLAP_ID"
[ -n "$new_vst3" ] || new_vst3="$OLD_VST3"
# VST3_CLASS_ID is a fixed 16-byte array, so it cannot simply be derived. Propose
# one from the new name and let a human decide - it is frozen the moment it ships.
proposed_vst3=$(printf '%s' "${new_struct}Plugin000000000000" | cut -c1-16)
n=$(printf '%s' "$new_vst3" | wc -c | tr -d ' ')
[ "$n" -eq 16 ] || die "VST3_CLASS_ID \"$new_vst3\" is $n bytes, must be exactly 16"
case "$new_clap_id" in
*\ *|*[A-Z]*) die "CLAP_ID \"$new_clap_id\" must be lowercase with no spaces" ;;
*.*) ;;
*) die "CLAP_ID \"$new_clap_id\" should be dotted reverse-DNS" ;;
esac
# CLAP_DESCRIPTION is user-facing: keep development history out of it.
for hist in "$CODENAME" "$OLD_DISPLAY" "$OLD_PKG"; do
case "$new_clap_description" in
*"$hist"*) die "CLAP_DESCRIPTION still mentions \"$hist\" - it is shown to users, who never knew the development name" ;;
esac
done
# --- plan -------------------------------------------------------------------
printf 'rename plan\n\n'
row() { [ "$2" = "$3" ] && printf ' %-18s %s (unchanged)\n' "$1" "$2" || printf ' %-18s %s -> %s\n' "$1" "$2" "$3"; }
row "package" "$OLD_PKG" "$new_pkg"
row "display" "$OLD_DISPLAY" "$new_display"
row "Plugin::NAME" "$OLD_PLUGIN_NAME" "$new_display"
row "vendor" "$OLD_VENDOR" "$new_vendor"
row "cargo author" "$OLD_AUTHOR" "$new_author"
row "email" "$OLD_EMAIL" "$new_email"
row "homepage" "$OLD_HOMEPAGE" "$new_homepage"
row "description" "$OLD_DESCRIPTION" "$new_description"
row "clap desc" "$OLD_CLAP_DESCRIPTION" "$new_clap_description"
row "struct" "$OLD_STRUCT" "$new_struct"
row "params struct" "$OLD_PARAMS_STRUCT" "$new_params_struct"
row "CLAP_ID" "$OLD_CLAP_ID" "$new_clap_id"
row "VST3_CLASS_ID" "$OLD_VST3" "$new_vst3"
printf '\n repo codename stays %s - fixed at creation, never renamed\n' "$CODENAME"
if [ "$new_vst3" = "$OLD_VST3" ]; then
printf ' suggested VST3_CLASS_ID for this name (16 bytes): "%s"\n' "$proposed_vst3"
printf ' apply with --vst3-class-id "%s" --allow-identity-change, and only before first release\n' "$proposed_vst3"
fi
if [ "$fail" -ne 0 ]; then
printf '\n%d problem(s); nothing written.\n' "$fail"
exit 1
fi
if [ "$apply" -eq 0 ]; then
printf '\nDry run. Re-run with --apply to write these changes.\n'
exit 0
fi
# --- apply ------------------------------------------------------------------
# Three scratch files, one trap: the regenerated site table, the rows carried
# through from the old one, and the value-substitution pairs.
gen="$(mktemp)"; keep="$(mktemp)"; subst_file="$(mktemp)"
trap 'rm -f "$gen" "$keep" "$subst_file"' EXIT
T="$(printf '\t')"
{
printf 'req%spkg.cargo%sCargo.toml%sname = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'req%spkg.bundler%sbundler.toml%s[%s]\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.lock%sCargo.lock%sname = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.ci%s.github/workflows/ci.yaml%scargo xtask bundle %s\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%spkg.readme%sREADME.md%scargo xtask bundle %s\n' "$T" "$T" "$T" "$new_pkg"
printf 'req%sdisplay.bundler%sbundler.toml%sname = "%s"\n' "$T" "$T" "$T" "$new_display"
printf 'opt%sdisplay.notice%sNOTICE%s%s\n' "$T" "$T" "$T" "$new_display"
printf 'req%sname.plugin%s%s%sconst NAME: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_display"
printf 'req%svendor.plugin%s%s%sconst VENDOR: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_vendor"
printf 'req%semail.plugin%s%s%sconst EMAIL: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_email"
printf 'req%sauthors.cargo%sCargo.toml%sauthors = ["%s <%s>"]\n' "$T" "$T" "$T" "$new_author" "$new_email"
printf 'req%shomepage.cargo%sCargo.toml%shomepage = "%s"\n' "$T" "$T" "$T" "$new_homepage"
printf 'req%surl.plugin%s%s%sconst URL: &'"'"'static str = env!("CARGO_PKG_HOMEPAGE")\n' "$T" "$T" "$LIB_FILE" "$T"
printf 'req%sdescription.cargo%sCargo.toml%sdescription = "%s"\n' "$T" "$T" "$T" "$new_description"
# Narrowest distinctive fragment, not the whole statement: rustfmt wraps a long
# CLAP_DESCRIPTION onto its own line, and a pattern spanning the assignment
# would then never match a correctly formatted file.
printf 'req%sdescription.clap%s%s%sSome("%s")\n' "$T" "$T" "$LIB_FILE" "$T" "$new_clap_description"
printf 'req%sclapid.plugin%s%s%sconst CLAP_ID: &'"'"'static str = "%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_clap_id"
printf 'req%svst3id.plugin%s%s%sconst VST3_CLASS_ID: [u8; 16] = *b"%s"\n' "$T" "$T" "$LIB_FILE" "$T" "$new_vst3"
printf 'req%sstruct.def%s%s%sstruct %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.plugin%s%s%simpl Plugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.clap%s%s%simpl ClapPlugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.vst3%s%s%simpl Vst3Plugin for %s {\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.exportclap%s%s%snih_export_clap!(%s);\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sstruct.exportvst3%s%s%snih_export_vst3!(%s);\n' "$T" "$T" "$LIB_FILE" "$T" "$new_struct"
printf 'req%sparams.def%s%s%sstruct %s {\n' "$T" "$T" "$PARAMS_FILE" "$T" "$new_params_struct"
printf 'req%sparams.default%s%s%simpl Default for %s {\n' "$T" "$T" "$PARAMS_FILE" "$T" "$new_params_struct"
printf 'req%sparams.field%s%s%sparams: Arc<%s>,\n' "$T" "$T" "$LIB_FILE" "$T" "$new_params_struct"
printf 'opt%sdeploy.pkg%sdeploy.ps1%s$Package = "%s"\n' "$T" "$T" "$T" "$new_pkg"
printf 'opt%sdeploy.vst3%sdeploy.ps1%s$vst3Name = "%s.vst3"\n' "$T" "$T" "$T" "$new_display"
printf 'opt%sdeploy.clap%sdeploy.ps1%s$clapName = "%s.clap"\n' "$T" "$T" "$T" "$new_display"
} > "$gen"
# Pass 1 - site-directed. Each owned row in the existing table carries the OLD
# resolved literal for its site; the regenerated row carries the new one. Rewriting
# by that pair is unambiguous, which plain value substitution is not: AUTHOR
# defaults to VENDOR, so both fields can hold the same old string and a global
# replace cannot tell which occurrence belongs to which. This must run BEFORE
# pass 2, or the global replace consumes the old literals first and leaves the
# precise rewrite with nothing to match.
sites="tools/branding-sites.tsv"
T="$(printf '\t')"
if [ -f "$sites" ]; then
while IFS="$T" read -r ok_ oo_ of_ op_; do
case "$ok_" in ''|\#*) continue ;; esac
np_=$(awk -F"$T" -v o="$oo_" '$2==o {print $4; exit}' "$gen")
[ -n "$np_" ] || continue
[ "$np_" != "$op_" ] || continue
[ -f "$of_" ] || continue
esc_o=$(printf '%s' "$op_" | sed 's/[][\.*^$/|&]/\\&/g')
esc_n=$(printf '%s' "$np_" | sed 's/[\\/&|]/\\&/g')
tmp_=$(mktemp)
sed "s|$esc_o|$esc_n|g" "$of_" > "$tmp_" && cat "$tmp_" > "$of_"
rm -f "$tmp_"
done < "$sites"
printf ' pass 1: rewrote known sites from the table\n'
fi
# Pass 2 - value substitution, for everything the table does not enumerate:
# doc comments, use sites in other modules, prose in the README.
# Longest values first: a shorter spelling can be a substring of a longer one
# (the package name inside a URL, say), and replacing it first would corrupt the
# longer match. branding.conf is excluded and rewritten wholesale below, so its
# STALE list is never rewritten into meaninglessness.
{
printf '%s\t%s\n' "$OLD_CLAP_ID" "$new_clap_id"
printf '%s\t%s\n' "$OLD_HOMEPAGE" "$new_homepage"
printf '%s\t%s\n' "$OLD_CLAP_DESCRIPTION" "$new_clap_description"
printf '%s\t%s\n' "$OLD_DESCRIPTION" "$new_description"
printf '%s\t%s\n' "$OLD_PARAMS_STRUCT" "$new_params_struct"
printf '%s\t%s\n' "$OLD_VST3" "$new_vst3"
printf '%s\t%s\n' "$OLD_PLUGIN_NAME" "$new_display"
printf '%s\t%s\n' "$OLD_DISPLAY" "$new_display"
printf '%s\t%s\n' "$OLD_STRUCT" "$new_struct"
printf '%s\t%s\n' "$OLD_EMAIL" "$new_email"
printf '%s\t%s\n' "$OLD_AUTHOR" "$new_author"
printf '%s\t%s\n' "$OLD_VENDOR" "$new_vendor"
printf '%s\t%s\n' "$OLD_PKG" "$new_pkg"
} | awk -F'\t' '$1 != $2 { print length($1)"\t"$0 }' | sort -rn | cut -f2- > "$subst_file"
changed=0
git ls-files 2>/dev/null | while IFS= read -r f; do
[ -f "$f" ] || continue
# branding.conf alone is excluded: it is rewritten wholesale below, and its
# STALE list names every retired spelling by design. The site table IS
# substituted, so hand-added custom rows track the rename; owned rows are
# regenerated afterwards regardless.
case "$f" in tools/branding.conf) continue ;; esac
before=$(cat "$f")
after="$before"
while IFS="$(printf '\t')" read -r old new; do
[ -n "$old" ] || continue
after=$(printf '%s' "$after" | sed "s|$(printf '%s' "$old" | sed 's/[][\.*^$/|&]/\\&/g')|$(printf '%s' "$new" | sed 's/[\\/&|]/\\&/g')|g")
done < "$subst_file"
if [ "$before" != "$after" ]; then printf '%s\n' "$after" > "$f"; printf ' rewrote %s\n' "$f"; fi
done
# branding.conf last, with the retired spellings appended to STALE so the next
# run of the checker sweeps for anything this pass missed.
# Deduplicated: two fields can retire to the same spelling - DISPLAY and
# PLUGIN_NAME are commonly identical - and listing it twice makes the checker
# report the same drift twice. Note that neither this script nor the checker
# contains a branding literal anywhere: both read every name from
# branding.conf, so a rename never has to rewrite its own tooling.
retired=$(cut -f1 "$subst_file" | awk '!seen[$0]++')
sed -e "s|^PKG=.*|PKG=\"$new_pkg\"|" \
-e "s|^DISPLAY=.*|DISPLAY=\"$new_display\"|" \
-e "s|^PLUGIN_NAME=.*|PLUGIN_NAME=\"$new_display\"|" \
-e "s|^VENDOR=.*|VENDOR=\"$new_vendor\"|" \
-e "s|^# *AUTHOR=.*|AUTHOR=\"$new_author\"|" \
-e "s|^AUTHOR=.*|AUTHOR=\"$new_author\"|" \
-e "s|^EMAIL=.*|EMAIL=\"$new_email\"|" \
-e "s|^HOMEPAGE=.*|HOMEPAGE=\"$new_homepage\"|" \
-e "s|^DESCRIPTION=.*|DESCRIPTION=\"$new_description\"|" \
-e "s|^CLAP_DESCRIPTION=.*|CLAP_DESCRIPTION=\"$new_clap_description\"|" \
-e "s|^CLAP_ID=.*|CLAP_ID=\"$new_clap_id\"|" \
-e "s|^VST3_CLASS_ID=.*|VST3_CLASS_ID=\"$new_vst3\"|" \
-e "s|^STRUCT=.*|STRUCT=\"$new_struct\"|" \
-e "s|^PARAMS_STRUCT=.*|PARAMS_STRUCT=\"$new_params_struct\"|" \
tools/branding.conf > tools/branding.conf.new
if [ -n "$retired" ]; then
{ sed '$ s/"$//' tools/branding.conf.new; printf '%s"\n' "$retired"; } > tools/branding.conf
else
# Nothing retired (a no-op rename, or a regeneration): leave STALE untouched
# rather than appending a blank line to it on every run.
cat tools/branding.conf.new > tools/branding.conf
fi
rm -f tools/branding.conf.new
if [ "$new_author" = "$new_vendor" ]; then
# Identical to VENDOR, so re-comment it: the conf should keep showing that this
# value derives rather than pinning a duplicate that then has to be maintained.
tmp_c=$(mktemp)
sed "s|^AUTHOR=.*|# AUTHOR=\"$new_author\"|" tools/branding.conf > "$tmp_c" && cat "$tmp_c" > tools/branding.conf
rm -f "$tmp_c"
fi
printf ' rewrote tools/branding.conf (retired spellings appended to STALE)\n'
# Regenerate the site table. Rows whose owner this script recognises are
# rewritten with the new resolved literals; every other row - anything marked
# custom, or added by hand for a file this template does not ship - is carried
# through untouched. Clobbering those silently is how a tool stops being trusted.
sites="tools/branding-sites.tsv"
owned=$(cut -f2 "$gen")
if [ -f "$sites" ]; then
grep '^#' "$sites" > "$keep" 2>/dev/null || : > "$keep"
while IFS="$T" read -r k o f pat; do
case "$k" in ''|\#*) continue ;; esac
printf '%s\n' "$owned" | grep -qxF "$o" || printf '%s%s%s%s%s%s%s\n' "$k" "$T" "$o" "$T" "$f" "$T" "$pat" >> "$keep"
done < "$sites"
else
: > "$keep"
fi
{ cat "$keep"; cat "$gen"; } > "$sites"
printf ' rewrote %s (%s owned rows, %s preserved)\n' "$sites" "$(wc -l < "$gen" | tr -d ' ')" "$(grep -cv '^#' "$keep" 2>/dev/null || echo 0)"
printf '\nverifying with check-branding.sh\n\n'
sh tools/check-branding.sh
+45
View File
@@ -0,0 +1,45 @@
# branding-sites.tsv - every place a canonical name has to appear, and in what form.
#
# Generated and refreshed by tools/brand-plugin.sh; read by tools/check-branding.sh,
# which asserts each pattern is present in its file. Tab-separated, four columns:
#
# kind req = a missing file is a failure; opt = a missing file is a skip
# owner the brand-plugin.sh rule that manages this row, or "custom" for rows
# you added by hand. Custom rows are preserved across regeneration;
# owned rows are overwritten, so do not hand-edit those.
# file repo-relative path
# pattern literal text that must appear in that file, already resolved
#
# Adding a site for something the template does not ship is a one-line edit here
# with owner "custom" - no shell to change, and it survives the next rename.
#
#kind owner file pattern
req pkg.cargo Cargo.toml name = "plugin_name"
req pkg.bundler bundler.toml [plugin_name]
opt pkg.lock Cargo.lock name = "plugin_name"
opt pkg.ci .github/workflows/ci.yaml cargo xtask bundle plugin_name
opt pkg.readme README.md cargo xtask bundle plugin_name
req display.bundler bundler.toml name = "Plugin Name"
opt display.notice NOTICE Plugin Name
req name.plugin src/lib.rs const NAME: &'static str = "Plugin Name"
req vendor.plugin src/lib.rs const VENDOR: &'static str = "Your Name"
req email.plugin src/lib.rs const EMAIL: &'static str = "your@email.com"
req authors.cargo Cargo.toml authors = ["Your Name <your@email.com>"]
req homepage.cargo Cargo.toml homepage = "https://wiki.novoyuuparosk.org"
req url.plugin src/lib.rs const URL: &'static str = env!("CARGO_PKG_HOMEPAGE")
req description.cargo Cargo.toml description = "A minimal NIH-plug audio plugin"
req description.clap src/lib.rs Some("Your plugin description")
req clapid.plugin src/lib.rs const CLAP_ID: &'static str = "com.your-domain.plugin-name"
req vst3id.plugin src/lib.rs const VST3_CLASS_ID: [u8; 16] = *b"Unique16CharId!!"
req struct.def src/lib.rs struct PluginName {
req struct.plugin src/lib.rs impl Plugin for PluginName {
req struct.clap src/lib.rs impl ClapPlugin for PluginName {
req struct.vst3 src/lib.rs impl Vst3Plugin for PluginName {
req struct.exportclap src/lib.rs nih_export_clap!(PluginName);
req struct.exportvst3 src/lib.rs nih_export_vst3!(PluginName);
req params.def src/lib.rs struct PluginNameParams {
req params.default src/lib.rs impl Default for PluginNameParams {
req params.field src/lib.rs params: Arc<PluginNameParams>,
opt deploy.pkg deploy.ps1 $Package = "plugin_name"
opt deploy.vst3 deploy.ps1 $vst3Name = "Plugin Name.vst3"
opt deploy.clap deploy.ps1 $clapName = "Plugin Name.clap"
Can't render this file because it contains an unexpected character in line 7 and column 64.
+99
View File
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# branding.conf - this plugin's canonical identity.
#
# Managed by tools/brand-plugin.sh, read by tools/check-branding.sh. Hand-editing
# works and is sometimes the quickest thing, but the helper is what keeps this
# file, the source tree and tools/branding-sites.tsv agreeing with each other.
# Edit here and you are on the hook for running the checker afterwards.
# --- derivations ------------------------------------------------------------
# Defined before the values so the defaults below can use them.
# Package name: lowercased, every run of non-alphanumerics becomes one
# underscore. Cargo accepts hyphens too and maps them to underscores for the
# built artifact, which nih_plug_xtask already handles; underscores are used to
# match the Rust convention.
_derive_pkg() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -e 's/[^a-z0-9]\{1,\}/_/g' -e 's/^_//' -e 's/_$//'
}
# Rust type name: CamelCase, non-alphanumerics dropped.
_derive_struct() {
printf '%s' "$1" | sed -e 's/[^A-Za-z0-9]\{1,\}/ /g' \
| awk '{for(i=1;i<=NF;i++) printf "%s%s", toupper(substr($i,1,1)), substr($i,2); print ""}'
}
# --- the names ---------------------------------------------------------------
PLUGIN_NAME="Plugin Name"
VENDOR="Your Name"
EMAIL="your@email.com"
# Cargo's authors entry names whoever wrote it, which is not always the vendor:
# an org ships the plugin, a person authored it. Defaults to VENDOR when the two
# are the same, which is the common case for a one-person project.
# AUTHOR="Your Name"
HOMEPAGE="https://wiki.novoyuuparosk.org"
DESCRIPTION="A minimal NIH-plug audio plugin"
# What the plugin does *now*. No codename, no "prototype", no "v2 rewrite" -
# hosts show this to users who never knew any of that. check-branding.sh
# enforces the mechanical half by rejecting retired names and the repo codename.
CLAP_DESCRIPTION="Your plugin description"
# --- frozen identity ---------------------------------------------------------
# Hosts key saved projects and presets off these two. Change either and every
# existing session silently loses the plugin: the track comes up empty, with no
# error. Choose once before the first release, then never again.
# brand-plugin.sh refuses to move them without --allow-identity-change.
CLAP_ID="com.your-domain.plugin-name"
VST3_CLASS_ID="Unique16CharId!!"
# --- fixed at repo creation --------------------------------------------------
# The repository's own name. Deliberately NOT renamed when the plugin is: the
# repo keeps its development codename while the plugin gets a product name.
# Recorded only so it can be kept out of user-facing text.
CODENAME="nih-plug-template"
# Where the two Rust types are defined. A plugin whose params outgrow lib.rs
# points PARAMS_FILE at src/params.rs.
LIB_FILE="src/lib.rs"
PARAMS_FILE="src/lib.rs"
# --- derived unless overridden -----------------------------------------------
# Set one only when the derivation is wrong or rejected - a name like
# "206 Prototype" derives to "206_prototype", which cargo refuses because a
# package name cannot start with a digit.
#
# This template overrides three of them, which a real plugin usually will not:
# its package and type names are the template's own rather than derived from a
# product name. Delete these three lines after renaming and the derivations
# from PLUGIN_NAME take over.
PKG="plugin_name"
STRUCT="PluginName"
PARAMS_STRUCT="PluginNameParams"
# DISPLAY="Plugin Name" # defaults to PLUGIN_NAME
: "${AUTHOR:=$VENDOR}"
: "${DISPLAY:=$PLUGIN_NAME}"
: "${PKG:=$(_derive_pkg "$PLUGIN_NAME")}"
: "${STRUCT:=$(_derive_struct "$PLUGIN_NAME")}"
: "${PARAMS_STRUCT:=${STRUCT}Params}"
# --- retired spellings -------------------------------------------------------
# Must no longer appear in tracked files. An entry is skipped while it is still
# part of a canonical value above and arms the moment it stops being one, so a
# rename is self-checking in both directions. brand-plugin.sh appends here.
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
https://github.com/YOUR-USERNAME/YOUR-PLUGIN-NAME
nih_plug_template
PluginParams
MyPlugin"
+188
View File
@@ -0,0 +1,188 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# 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 tools/check-branding.sh (exit 0 = consistent, 1 = drift found)
#
# The canonical identity lives in branding.conf, shared with brand-plugin.sh.
# Edit it there, then run this to find every copy that still holds the old value.
set -u
# Resolve the repo root: these live in tools/, every path they touch is
# repo-relative, and they must behave the same wherever they are invoked from.
cd "$(dirname "$0")/.." || exit 1
if [ ! -f tools/branding.conf ]; then
printf 'tools/branding.conf not found\n' >&2
exit 1
fi
# shellcheck disable=SC1091
. ./tools/branding.conf
# 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"
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"
# --- consistency: every site in the table holds its resolved literal ----------
# The table is data, so adding a site is a row rather than a shell edit, and the
# same script runs unmodified across every plugin built from this template.
SITES="tools/branding-sites.tsv"
if [ ! -f "$SITES" ]; then
bad "site table" "$SITES not found - run tools/brand-plugin.sh to generate it"
else
printf 'sites\n'
while IFS="$(printf '\t')" read -r kind owner file pattern; do
case "$kind" in ''|\#*) continue ;; esac
[ -n "$pattern" ] || continue
label="$owner"
if [ ! -f "$file" ]; then
case "$kind" in
opt) note "$label ($file not present)" ;;
*) bad "$label" "missing file: $file" ;;
esac
continue
fi
if has "$file" "$pattern"; then ok "$label"
else bad "$label" "$file does not contain: $pattern"; fi
done < "$SITES"
fi
# --- validity: rules the table cannot express --------------------------------
printf '\nvalidity\n'
# VST3 class IDs are a fixed 16-byte array. A wrong length is a compile error,
# but saying so here is clearer than reading 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 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
# An overridden package name is legitimate - the derivation is sometimes invalid
# - but a display rename that forgot the package is not, and looks identical
# from here. Reported, never failed.
derived_pkg=$(_derive_pkg "$PLUGIN_NAME")
[ "$PKG" = "$derived_pkg" ] || info "PKG \"$PKG\" differs from the derivation of PLUGIN_NAME (\"$derived_pkg\") - fine if deliberate"
# CLAP_DESCRIPTION is shown to users in a host's plugin browser. "Free of
# development history" is a judgement a script cannot make, but the failure it
# produces is mechanical: a retired name or the repo codename surviving in it.
hygiene_hits=""
while IFS= read -r s_entry; do
[ -n "$s_entry" ] || continue
case "$CANONICAL_BLOB" in *"$s_entry"*) continue ;; esac
case "$CLAP_DESCRIPTION" in *"$s_entry"*) hygiene_hits="$hygiene_hits \"$s_entry\"" ;; esac
done <<HYGIENE_LIST
$STALE
$CODENAME
HYGIENE_LIST
if [ -z "$hygiene_hits" ]; then ok "CLAP_DESCRIPTION carries no retired name or codename"
else bad "CLAP_DESCRIPTION carries no retired name or codename" "found:$hygiene_hits"; fi
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"
# 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
# These two name every retired spelling by definition - the STALE list lives in
# branding.conf and this script reads it - so neither can ever be a real hit.
hits=$(file_list 2>/dev/null | while read -r f; do
case "$f" in tools/check-branding.sh|tools/branding.conf) continue ;; esac
[ -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
+1
View File
@@ -2,6 +2,7 @@
name = "xtask"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "f36931f7af4646065488a9845d8f8c2f95252c23" }
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-or-later
fn main() -> nih_plug_xtask::Result<()> {
nih_plug_xtask::main()