Compare commits

...

23 Commits

Author SHA1 Message Date
Mikkeli Matlock 74dfdc50cc Merge branch 'fix/gitattributes-stray-cr' 2026-09-14 01:41:30 +09:00
Mikkeli Matlock 94d41d5c8a 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 repository
that the new LF policy could not normalise.

The rules themselves were never affected - the byte sat inside a comment, and
deploy.bat and the sources were already checking out with the intended endings.

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:30 +09:00
Mikkeli Matlock b27ef57676 Merge branch 'chore/lf-line-endings' 2026-09-14 01:39:09 +09:00
Mikkeli Matlock c940f31ec7 chore: pin LF line endings in .gitattributes
The branding checker failed all 29 of its sites on Windows while passing on the
Linux CI runner. Nothing had drifted. Git for Windows installs with
core.autocrlf=true, which checked tools/branding-sites.tsv out as CRLF, and the
checker reads that table with `read` - so every row's last field, the pattern,
gained a trailing \r and matched nothing. Only *.sh was pinned to LF, which is
why the scripts were fine and their data was not.

Set the policy in the repository rather than trusting each machine:

    * 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 a text file
checks out LF and a .bat checks out CRLF.

No blob changes. Every committed blob was already LF (git ls-files --eol shows
i/lf throughout) and `git add --renormalize .` stages nothing but this file.
The working tree was re-checked-out to match: 25 files CRLF -> LF, deploy.bat
LF -> CRLF. Checker 40 passed / 0 failed, build clean, 20 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-09-14 01:39:09 +09:00
mikkeli a8f1244b00 Merge branch 'ci/validate-workflow-changes'
CI / Build (push) Successful in 30s
Include the workflow file in both path filters so CI changes are self-validating.
2026-09-04 19:33:12 +09:00
mikkeli d107127017 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 d51de80de8 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 7a92485cf1 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 6c0332d79e Merge branch 'fix/f32-literal-fallback'
CI / Build (push) Successful in 24s
Suffix the f32 literals rustc is phasing out, and deny future_incompatible so the
next deferred hard error fails the build immediately.
2026-09-03 08:25:36 +09:00
mikkeli 309a152d58 fix: suffix f32 literals and deny future-incompatible warnings
Sixteen `Stroke::new(1.0, ...)` calls in the editor relied on a numeric
inference fallback that rustc is removing:

  warning: falling back to `f32` as the trait bound `f32: From<f64>` is
  not satisfied
  = warning: this was previously accepted by the compiler but is being
    phased out; it will become a hard error in a future release!

These were never clippy lints - a plain `cargo build` emitted all sixteen
- and they are not style. They are a dated compile break: the code stops
building on whichever release drops the fallback. Applied via `cargo fix`,
since rustc marks the suggestion machine-applicable; the diff is sixteen
literals gaining an `_f32` suffix and nothing else. The values were
already f32, just inferred.

Denying the future_incompatible group makes the next one a build failure
the day it appears rather than something that accumulates unnoticed until
a toolchain bump breaks the build on a day nobody chose. Verified by
reverting one literal: the build then fails with exit 101 instead of
warning.

Pinning rust-toolchain.toml was considered and rejected for this purpose.
It buys reproducibility, but it works against early warning - a pinned
toolchain finds out about the next deferred hard error later, not sooner.

Build warnings drop from 17 to 0. The 15 remaining clippy lints are
genuine style suggestions and are untouched, as is clippy's absence from
CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-03 08:25:36 +09:00
mikkeli a62efe44b3 Merge branch 'feat/branding-toolkit'
CI / Build (push) Successful in 21s
Adopt the template's four-file branding toolkit under tools/, with this plugin's
identity as data rather than forked shell.
2026-09-02 16:39:12 +09:00
mikkeli 3a144064e3 feat: adopt the template's branding toolkit
Replaces the single check-branding.sh with the four-file toolkit from
musiikki/nih-plug-template, moved under tools/ alongside where the deploy
scripts will eventually live.

The two scripts are byte-identical to the template's; everything specific
to this plugin lives in tools/branding.conf and tools/branding-sites.tsv.
That is the point of the split: syncing an improvement is now a file copy
rather than a shell merge, which is what the last two syncs had become.

This repo's data differs from the template's in ways that exercise the
design rather than fight it: params live in src/params.rs rather than
src/lib.rs, deploy.ps1 exists so its three rows check instead of skip, and
the CI bundle command carries no --features flag because VST3 is always on
here. All of that is table data now, not forked shell.

Nothing needed overriding: PKG, STRUCT and PARAMS_STRUCT all derive
correctly from PLUGIN_NAME ("Bari 206" -> bari_206 / Bari206 /
Bari206Params). AUTHOR is pinned because this plugin separates the person
who wrote it from the organisation that vends it — a distinction the
template could not express until integrating here surfaced it.

Adopting it required two upstream fixes, both made in the template first
so the scripts stay identical: cargo AUTHOR as a value distinct from
Plugin::VENDOR, and site patterns narrowed to the smallest distinctive
fragment after this repo's rustfmt-wrapped CLAP_DESCRIPTION proved a
statement-spanning pattern cannot match correctly formatted source.

CI is repointed at tools/check-branding.sh, and its path filter widened to
tools/** — the old filter named a file that no longer exists, so a change
to the toolkit would have triggered no CI at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-09-02 16:39:12 +09:00
Mikkeli Matlock 3161aa9388 chore: seeded colours
CI / Build (push) Successful in 19s
2026-09-02 16:31:16 +09:00
Mikkeli Matlock d87934c428 Merge branch 'feat/palette'
CI / Build (push) Successful in 19s
2026-08-31 23:28:26 +09:00
Mikkeli Matlock 1361d7c919 feat: extract a two-tier colour palette
Colour was 35 call sites and ~29 distinct literals spread across four widget
files, with no egui Style or Visuals set at all - so the hand-painted widgets
were hand-coloured while ParamSlider and the labels sat on stock egui defaults,
which is most of why the UI did not read as one thing.

The audit turned up more than duplication. Two different panel blacks
((16,16,20) and (20,20,24)) for the same intended surface; three reds, two
greens and two blues close enough to look like mistakes; eight greys doing
about four jobs. And a genuine collision: blue/green/orange meant LOW/MID/HIGH
in the crossover and input/output/gain-reduction in the scope, so orange meant
"the high band" in one widget and "gain reduction" in the one beside it.

The rule that resolves it, and that the module documents: channel hue is
chrome, functional colour is data. A pixel showing a value comes from the
functional palette and means the same thing in every channel; a pixel saying
which channel you are looking at comes from that channel's hue and never
touches the data. The scope turns out not to need arbitration at all - it is
tabbed, one channel at a time, so its three traces never compete with a hue.

Two tiers. Seeds are a short hand-picked list: four channel hues, the
functional colours, six neutrals. Everything else derives from a hue via
`channel()`, so recolouring a channel is one constant. Derivation is const fn -
including the HSV conversion, written out because ecolor's is not const - so
the whole table is built at compile time and costs nothing at runtime.

Chosen so the hues describe the colours already in use: the derived crossover
strips land within 1-3 units of the literals they replace. The two drag handles
shift by ~17 on one channel, being now consistently saturated with the rest.

Also lands the one deliberate visual change: the meter's four cells were
identical and distinguishable only by the label underneath, which is where
channel identity was hardest to read. They now carry a dark channel wash and an
accent rule, while the bars stay strictly functional.

Visuals are wired from the same seeds in the editor's build closure, so egui's
own widgets stop looking foreign. meter.rs's local level_color is gone; the
palette owns the headroom ramp rather than there being two places to edit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-08-31 23:28:26 +09:00
Mikkeli Matlock 6ab9fb9c3f Merge branch 'rename/bari-206'
CI / Build (push) Successful in 20s
2026-08-31 22:55:41 +09:00
Mikkeli Matlock 37f34e1175 rename: Codename 206 -> Bari 206
Retires the working codename now rather than at 1.0, because the two identity
constants a DAW uses to recognise a plugin across sessions can only be changed
for free while nothing has saved a project with it. Verified: zero references
to the plugin across all 69 FL project backups, under any identity string.

Derived spellings:
    Cargo package     codename_206        -> bari_206
    bundle / DAW name Codename 206 |
                      206 prototype       -> Bari 206      (unified; drops "prototype")
    Rust types        Codename206(Params) -> Bari206(Params)
    CLAP_ID           com.mikkeli.codename-206
                                          -> org.novoyuuparosk.bari-206
    VST3_CLASS_ID     Codename206Maxi!    -> Bari206Multiband

CLAP_ID moves to reverse-DNS on a domain actually controlled, and stops
disagreeing with a VENDOR that was already Novoyuuparosk. VST3_CLASS_ID stays
exactly 16 bytes and drops "Maxi", which sat close to Image-Line's Maximus and
Maximizer in the same product category.

Author identity converges on Mikkeli Matlock <mikkeli@novoyuuparosk.org>;
Cargo authors had been carrying a personal gmail address while Plugin::EMAIL
already used the org one. VENDOR stays Novoyuuparosk, which is the org rather
than the person, and copyright stays with the person.

Deliberately unchanged: the repo, its remote, the local directory and the
homepage URL all keep "codename-206". Renaming those buys nothing and costs a
Gitea rename, a remote rewrite and a directory move, so the bare "codename-206"
is current branding rather than residue - and is the one spelling the STALE
list must not contain.

Driven by check-branding.sh rather than by grep: setting the canonical block
listed all 26 sites, including .github/workflows/ci.yaml, which no explicit
check covered. That gap is now closed by a positive check on the CI bundle
command, so a future rename fails in seconds instead of after a full compile.
STALE now carries both the retired and the new spellings, so the next rename
is self-checking too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-08-31 22:55:41 +09:00
mikkeli 73edf58b32 Merge branch 'ci/build-gate'
CI / Build (push) Successful in 22s
Add the CI gate migrated from nih-plug-template: branding check, build, test, bundle.
2026-08-31 22:34:31 +09:00
mikkeli 193cee0aef ci: gate pushes on a build, test and bundle
Migrated from musiikki/nih-plug-template, adapted for this repo: the
template gates VST3 behind an opt-in `vst3` feature and so builds twice,
while this plugin always ships VST3 (nih_plug is taken with its default
features), so one build covers both formats.

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. The bundle step is included because `cargo build` never exercises
the xtask bundler, and bundler.toml drifting from the crate name stays
invisible until you try to produce an artifact.

Performance is deliberately not gated. `Plugin::process()` cannot be
called from a test — `ProcessContext` requires a `&Transport`, whose
constructor and fields are `pub(crate)` to nih_plug — so any realtime
check would have to target the `src/dsp/` types directly, and a meaningful
one needs an audio deadline rather than a shared CI box.

Verified by running the exact step sequence as the gitea-runner user under
the service's environment: 28 branding checks, 20 unit tests, both bundles
produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-31 22:34:31 +09:00
mikkeli 814dd5eacd Merge branch 'chore/relicense-gpl3'
Restore GPL-3.0-or-later, which the VST3 build always required, and add the
ISC attribution owed to the NIH-plug upstream.
2026-08-31 22:33:08 +09:00
mikkeli 466ab478c9 chore: relicense to GPL-3.0-or-later and credit the ISC upstream
Commit 057d630 switched this repo from GPL-3.0 to Apache-2.0, which the
build contradicts: src/lib.rs calls nih_export_vst3! unconditionally and
nih_plug is taken with default features, so every build links vst3-sys —
GPLv3-or-later by its own authors' choice. deploy.ps1 then installs that
binary. The declared licence and the artifact's actual terms disagreed,
and the original GPL-3.0 was correct.

Given the intent to distribute non-profit and open-source, GPL costs
nothing here, so this restores it rather than dropping VST3 to keep a
permissive licence.

Also adds the ISC notice owed to Robbert van der Helm: the plugin
scaffolding derives from the NIH-plug cookiecutter template and example
plugins, whose one condition is that the copyright and permission notice
travel with all copies. The README credited the template under Reference
Material, which is courtesy, not compliance. ISC is GPL-compatible, so the
notice is retained in NOTICE rather than superseded.

Corrects the README's claim that Steinberg's SDK licence must be accepted
before distributing. No SDK source is vendored or redistributed, so there
is nothing to accept at build time. The binding constraint is vst3-sys's
GPLv3; Steinberg's trademark in "VST" is a separate regime 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:32:05 +09:00
Mikkeli Matlock 87a6f2e276 Merge branch 'fix/per-band-rms-window' 2026-08-31 22:10:52 +09:00
Mikkeli Matlock 570dd65fa8 fix: give each band its own RMS window instead of a global 5 ms
5 ms was too short to be an RMS average at all. Squaring the detector signal
puts a component at twice the fundamental, and a one-pole that does not span a
cycle passes it straight through, so the detector ripples and the gain
intermodulates with the bass. Measured peak-to-peak detector ripple at 5 ms:

     30 Hz  50 Hz  80 Hz  120 Hz  200 Hz  500 Hz  1 kHz
      4.42   2.72   1.72    1.15    0.69    0.28   0.14   dB

That lands squarely on the low band, whose crossover reaches down to 30 Hz,
and attack is allowed to go to 0 ms so nothing downstream is guaranteed to
mask it. Above ~500 Hz the same window was so short that RMS mode barely
differed from peak, making the mode switch do less than it appeared to.

The fix is per-band rather than one longer constant, because the required
window is set by how low the band reaches and a multiband already knows that.
Cost is response time (95% of a step: ~12 ms at 5 ms, ~117 ms at 50 ms), so
each band gets only what it needs:

    LOW  50 ms   30 Hz ripple 4.42 -> 0.46 dB; slow is wanted down here anyway
    MID  20 ms   body without dulling the band carrying most transients
    HIGH 10 ms   nothing up there ripples, so stay responsive
    ALL  35 ms   full-band, so it has to cover the low content too

RMS_WINDOW_MS moves from a compressor constant to a table in lib.rs indexed by
the band consts, and Compressor::prepare takes the window - the compressor has
no business knowing which band it is.

rms_window_suppresses_low_frequency_ripple pins the property: the LOW window
holds 30 Hz under 1 dB, a 5 ms window is far worse on the same signal (so the
bound cannot pass for the wrong reason), ripple falls monotonically with window
length, and the HIGH band's short window stays clean at 5 kHz.

This changes how the plugin sounds in RMS mode - needs an ear, not just tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-08-31 22:10:52 +09:00
30 changed files with 1930 additions and 497 deletions
+19 -3
View File
@@ -1,4 +1,20 @@
# Shell scripts must keep LF endings. core.autocrlf=true on Windows would # Line endings: LF in the repository and in every working tree, on every OS.
# otherwise check them out CRLF, which a strict POSIX sh (dash, CI containers) #
# rejects at the shebang. # 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 *.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
+71
View File
@@ -0,0 +1,71 @@
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.
#
# Adapted from musiikki/nih-plug-template. The template gates VST3 behind an
# opt-in `vst3` feature and so builds twice; this plugin always ships VST3
# (nih_plug is taken with default features), so one build covers both formats.
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
- name: Build
run: cargo build --release
- name: Test
run: cargo test --release
# 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 bari_206 --release
Generated
+8 -8
View File
@@ -147,6 +147,14 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "bari_206"
version = "0.1.0"
dependencies = [
"nih_plug",
"nih_plug_egui",
]
[[package]] [[package]]
name = "baseview" name = "baseview"
version = "0.1.0" version = "0.1.0"
@@ -337,14 +345,6 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "codename_206"
version = "0.1.0"
dependencies = [
"nih_plug",
"nih_plug_egui",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
+11 -3
View File
@@ -1,14 +1,14 @@
[package] [package]
name = "codename_206" name = "bari_206"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
authors = ["Mikkeli Matlock <matlockib@gmail.com>"] authors = ["Mikkeli Matlock <mikkeli@novoyuuparosk.org>"]
homepage = "https://github.com/mikkeli/codename-206" homepage = "https://github.com/mikkeli/codename-206"
description = "A VST3/CLAP multiband compressor/limiter with a custom gain-curve display" description = "A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
# VST3 is on by default in nih_plug, and its bindings (vst3-sys) are GPLv3. Any # VST3 is on by default in nih_plug, and its bindings (vst3-sys) are GPLv3. Any
# distributed VST3 binary built with them must therefore be GPL-3.0-or-later # distributed VST3 binary built with them must therefore be GPL-3.0-or-later
# (or covered by a commercial Steinberg licence). CLAP carries no such requirement. # (or covered by a commercial Steinberg licence). CLAP carries no such requirement.
license = "Apache-2.0" license = "GPL-3.0-or-later"
[workspace] [workspace]
members = ["xtask"] members = ["xtask"]
@@ -17,6 +17,14 @@ members = ["xtask"]
# `cdylib` is the plugin binary; `lib` (rlib) lets `cargo test` link the unit tests. # `cdylib` is the plugin binary; `lib` (rlib) lets `cargo test` link the unit tests.
crate-type = ["cdylib", "lib"] 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 16 of them 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] [dependencies]
# Pinned to the exact commit of the local ./nih-plug reference clone so the build # Pinned to the exact commit of the local ./nih-plug reference clone so the build
# is reproducible and matches the examples we read from. Bump the `rev` together # is reproducible and matches the examples we read from. Bump the `rev` together
+642 -169
View File
@@ -1,201 +1,674 @@
Apache License GNU GENERAL PUBLIC LICENSE
Version 2.0, January 2004 Version 3, 29 June 2007
http://www.apache.org/licenses/
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, The GNU General Public License is a free, copyleft license for
and distribution as defined by Sections 1 through 9 of this document. software and other kinds of works.
"Licensor" shall mean the copyright owner or entity authorized by The licenses for most software and other practical works are designed
the copyright owner that is granting the License. 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 When we speak of free software, we are referring to freedom, not
other entities that control, are controlled by, or are under common price. Our General Public Licenses are designed to make sure that you
control with that entity. For the purposes of this definition, have the freedom to distribute copies of free software (and charge for
"control" means (i) the power, direct or indirect, to cause the them if you wish), that you receive source code or can get it if you
direction or management of such entity, whether by contract or want it, that you can change the software or use pieces of it in new
otherwise, or (ii) ownership of fifty percent (50%) or more of the free programs, and that you know you can do these things.
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity To protect your rights, we need to prevent others from denying you
exercising permissions granted by this License. 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, For example, if you distribute copies of such a program, whether
including but not limited to software source code, documentation gratis or for a fee, you must pass on to the recipients the same
source, and configuration files. 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 Developers that use the GNU GPL protect your rights with two steps:
transformation or translation of a Source form, including but (1) assert copyright on the software, and (2) offer you this License
not limited to compiled object code, generated documentation, giving you legal permission to copy, distribute and/or modify it.
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or For the developers' and authors' protection, the GPL clearly explains
Object form, made available under the License, as indicated by a that there is no warranty for this free software. For both users' and
copyright notice that is included in or attached to the work authors' sake, the GPL requires that modified versions be marked as
(an example is provided in the Appendix below). 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 Some devices are designed to deny users access to install or run
form, that is based on (or derived from) the Work and for which the modified versions of the software inside them, although the manufacturer
editorial revisions, annotations, elaborations, or other modifications can do so. This is fundamentally incompatible with the aim of
represent, as a whole, an original work of authorship. For the purposes protecting users' freedom to change the software. The systematic
of this License, Derivative Works shall not include works that remain pattern of such abuse occurs in the area of products for individuals to
separable from, or merely link (or bind by name) to the interfaces of, use, which is precisely where it is most unacceptable. Therefore, we
the Work and Derivative Works thereof. 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 Finally, every program is threatened constantly by software patents.
the original version of the Work and any modifications or additions States should not allow patents to restrict development and use of
to that Work or Derivative Works thereof, that is intentionally software on general-purpose computers, but in those that do, we wish to
submitted to Licensor for inclusion in the Work by the copyright owner avoid the special danger that patents applied to a free program could
or by an individual or Legal Entity authorized to submit on behalf of make it effectively proprietary. To prevent this, the GPL assures that
the copyright owner. For the purposes of this definition, "submitted" patents cannot be used to render the program non-free.
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."
"Contributor" shall mean Licensor and any individual or Legal Entity The precise terms and conditions for copying, distribution and
on behalf of whom a Contribution has been received by Licensor and modification follow.
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of TERMS AND CONDITIONS
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.
3. Grant of Patent License. Subject to the terms and conditions of 0. Definitions.
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.
4. Redistribution. You may reproduce and distribute copies of the "This License" refers to version 3 of the GNU General Public License.
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:
(a) You must give any other recipients of the Work or "Copyright" also means copyright-like laws that apply to other kinds of
Derivative Works a copy of this License; and works, such as semiconductor masks.
(b) You must cause any modified files to carry prominent notices "The Program" refers to any copyrightable work licensed under this
stating that You changed the files; and 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 To "modify" a work means to copy from or adapt all or part of the work
that You distribute, all copyright, patent, trademark, and in a fashion requiring copyright permission, other than the making of an
attribution notices from the Source form of the Work, exact copy. The resulting work is called a "modified version" of the
excluding those notices that do not pertain to any part of earlier work or a work "based on" the earlier work.
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its A "covered work" means either the unmodified Program or a work based
distribution, then any Derivative Works that You distribute must on the Program.
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.
You may add Your own copyright statement to Your modifications and To "propagate" a work means to do anything with it that, without
may provide additional or different license terms and conditions permission, would make you directly or secondarily liable for
for use, reproduction, or distribution of Your modifications, or infringement under applicable copyright law, except executing it on a
for any such Derivative Works as a whole, provided Your use, computer or modifying a private copy. Propagation includes copying,
reproduction, and distribution of the Work otherwise complies with distribution (with or without modification), making available to the
the conditions stated in this License. public, and in some countries other activities as well.
5. Submission of Contributions. Unless You explicitly state otherwise, To "convey" a work means any kind of propagation that enables other
any Contribution intentionally submitted for inclusion in the Work parties to make or receive copies. Mere interaction with a user through
by You to the Licensor shall be under the terms and conditions of a computer network, with no transfer of a copy, is not conveying.
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.
6. Trademarks. This License does not grant permission to use the trade An interactive user interface displays "Appropriate Legal Notices"
names, trademarks, service marks, or product names of the Licensor, to the extent that it includes a convenient and prominently visible
except as required for reasonable and customary use in describing the feature that (1) displays an appropriate copyright notice, and (2)
origin of the Work and reproducing the content of the NOTICE file. 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 1. Source Code.
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.
8. Limitation of Liability. In no event and under no legal theory, The "source code" for a work means the preferred form of the work
whether in tort (including negligence), contract, or otherwise, for making modifications to it. "Object code" means any non-source
unless required by applicable law (such as deliberate and grossly form of a work.
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.
9. Accepting Warranty or Additional Liability. While redistributing A "Standard Interface" means an interface that either is an official
the Work or Derivative Works thereof, You may choose to offer, standard defined by a recognized standards body, or, in the case of
and charge a fee for, acceptance of support, warranty, indemnity, interfaces specified for a particular programming language, one that
or other liability obligations and/or rights consistent with this is widely used among developers working in that language.
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.
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 The Corresponding Source need not include anything that users
boilerplate notice, with the fields enclosed by brackets "[]" can regenerate automatically from other parts of the Corresponding
replaced with your own identifying information. (Don't include Source.
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.
Copyright 2026 Mikkeli Matlock The Corresponding Source for a work in source code form is that
same work.
Licensed under the Apache License, Version 2.0 (the "License"); 2. Basic Permissions.
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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 You may make, run and propagate covered works that you do not
distributed under the License is distributed on an "AS IS" BASIS, convey, without conditions so long as your license otherwise remains
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. in force. You may convey covered works to others for the sole purpose
See the License for the specific language governing permissions and of having them make modifications exclusively for you, or provide you
limitations under the License. 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>.
+37
View File
@@ -0,0 +1,37 @@
Bari 206
Copyright 2026 Mikkeli Matlock
Licensed under the GNU General Public License v3.0 or later.
See LICENSE for the full terms.
--------------------------------------------------------------------------------
Third-party material
--------------------------------------------------------------------------------
The plugin scaffolding derives from the NIH-plug cookiecutter template and
NIH-plug's example plugins, 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.
"VST" is a trademark of Steinberg Media Technologies GmbH.
+32 -7
View File
@@ -1,4 +1,4 @@
# Codename 206 # Bari 206
*Called 206 because the Peugeot 206 has a 'maxi' variant. You'll know this is a Maximizer knockoff if you can follow that trail of thoughts.* *Called 206 because the Peugeot 206 has a 'maxi' variant. You'll know this is a Maximizer knockoff if you can follow that trail of thoughts.*
Multiband Compressor / Limiter VST3 — Project Plan Multiband Compressor / Limiter VST3 — Project Plan
@@ -163,8 +163,8 @@ The project is already scaffolded (NIH-plug + nih_plug_egui, pinned to a fixed g
```powershell ```powershell
# Build + bundle the VST3 and CLAP # Build + bundle the VST3 and CLAP
cargo xtask bundle codename_206 --release cargo xtask bundle bari_206 --release
# Output: target\bundled\Codename 206.vst3 and Codename 206.clap # Output: target\bundled\Bari 206.vst3 and Bari 206.clap
``` ```
### Deployment ### Deployment
@@ -184,7 +184,7 @@ Use the provided script (no need to remember the details):
`deploy.bat` is a double-click wrapper around the same script. `deploy.bat` is a double-click wrapper around the same script.
After deploying, in FL Studio: **Options → Manage plugins → tick "Rescan previously failed After deploying, in FL Studio: **Options → Manage plugins → tick "Rescan previously failed
plugins" → Find installed plugins**, then search for **Codename 206**. (The rescan-failed step plugins" → Find installed plugins**, then search for **Bari 206**. (The rescan-failed step
is essential — without it FL silently skips a plugin it has seen before.) is essential — without it FL silently skips a plugin it has seen before.)
> **Known issue (deferred):** the **CLAP** build shows its name/vendor/type correctly in FL, but > **Known issue (deferred):** the **CLAP** build shows its name/vendor/type correctly in FL, but
@@ -277,12 +277,37 @@ the editor being open:
Never pass DSP state to the UI directly. Never pass DSP state to the UI directly.
### VST3 licensing ### VST3 licensing
You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries. This plugin is **GPL-3.0-or-later** (see `LICENSE`, and `NOTICE` for attribution).
NIH-plug's VST3 bindings are GPLv3; if you distribute, the plugin must also be GPLv3
(or you need a commercial Steinberg licence). CLAP has no such restriction. `nih_export_vst3!()` 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".
That is what makes a distributed VST3 build GPL-licensed, and it holds regardless of any
question about Steinberg's own rights.
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,
which no source licence resolves. CLAP carries neither encumbrance.
--- ---
## CI
`.github/workflows/ci.yaml` gates pushes and PRs on the branding check, a release build,
`cargo test`, and a bundle — enough to catch a rename that only half-landed, a build break,
or a `bundler.toml` that no longer matches the crate name.
It targets `runs-on: rust-audio`, a self-hosted host-mode runner registered to the `musiikki`
org. That label is org-scoped: a fork outside `musiikki` will not see the runner and its jobs
will queue indefinitely rather than fail.
Plugin performance is deliberately not gated. 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. The DSP
types under `src/dsp/` take no nih-plug context and are directly testable if you need it.
---
## Reference Material ## Reference Material
- [NIH-plug repo](https://github.com/robbert-vdh/nih-plug) — read the `plugins/` examples first - [NIH-plug repo](https://github.com/robbert-vdh/nih-plug) — read the `plugins/` examples first
+2 -2
View File
@@ -3,5 +3,5 @@
# [package_name] # [package_name]
# name = "Human Readable Plugin Name" # defaults to <package_name> # name = "Human Readable Plugin Name" # defaults to <package_name>
[codename_206] [bari_206]
name = "Codename 206" name = "Bari 206"
-200
View File
@@ -1,200 +0,0 @@
#!/bin/sh
# check-branding.sh - verify the plugin's branding/identity strings agree everywhere.
#
# Plugin branding is duplicated across the build manifest, the bundler manifest,
# the source, the docs and 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.
#
# Kept in sync with musiikki/nih-plug-template's copy - fixes belong there
# first, then come back here. The differences are structural, not stylistic:
# this repo keeps its params in src/params.rs and wraps CLAP_DESCRIPTION.
#
# Usage: sh check-branding.sh (exit 0 = consistent, 1 = drift found)
#
# ---------------------------------------------------------------------------
# CANONICAL IDENTITY - the single source of truth. Edit these to rename, then
# run this script; every failure it prints is a place still holding the old name.
# ---------------------------------------------------------------------------
PKG="codename_206" # Cargo package name == bundler.toml table key
DISPLAY="Codename 206" # bundle name -> "<DISPLAY>.vst3" / "<DISPLAY>.clap"
PLUGIN_NAME="206 prototype" # Plugin::NAME - what the DAW displays
VENDOR="Novoyuuparosk" # Plugin::VENDOR
EMAIL="mikkeli@novoyuuparosk.org" # Plugin::EMAIL
HOMEPAGE="https://github.com/mikkeli/codename-206" # Cargo homepage -> Plugin::URL
CLAP_ID="com.mikkeli.codename-206" # CLAP_ID - stable plugin identity
VST3_CLASS_ID="Codename206Maxi!" # VST3_CLASS_ID - stable identity, MUST be 16 bytes
DESCRIPTION="A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
CLAP_DESCRIPTION="Multiband compressor/limiter (stage 3: 3-band + 'All' channel)"
# Rust type names. Cosmetic - they never reach a host - but a rename that leaves
# half of them behind is the residue that says nobody read the file.
STRUCT="Codename206" # the Plugin impl target (src/lib.rs)
PARAMS_STRUCT="Codename206Params" # the Params impl target (src/params.rs)
# Seeded with this repo's CURRENT names, which is the point: each entry is
# skipped while it is still canonical above, and arms itself the moment you
# change that value. So the planned "codename 206" -> "206" rename is
# self-checking - every spelling you miss turns into a FAIL here on the next run.
STALE="codename_206
codename-206
Codename 206
Codename206
Codename206Params
Codename206Maxi!
206 prototype
com.mikkeli.codename-206"
set -u
cd "$(dirname "$0")" || exit 1
pass=0; fail=0; skip=0
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail+1)); printf ' FAIL %s\n' "$1"; [ $# -gt 1 ] && printf ' %s\n' "$2"; return 0; }
note() { skip=$((skip+1)); printf ' skip %s\n' "$1"; }
# Reported for awareness only - never counted, never fails the run.
info() { printf ' -- %s\n' "$1"; }
# has FILE PATTERN -> fixed-string search
has() { grep -qF "$2" "$1" 2>/dev/null; }
check() { # check LABEL FILE LITERAL
if [ ! -f "$2" ]; then bad "$1" "missing file: $2"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
# Same as check, but a missing file is "not applicable yet" rather than drift.
# The template ships without a deploy script; this starts checking once you add one.
opt_check() { # opt_check LABEL FILE LITERAL
if [ ! -f "$2" ]; then note "$1 ($2 not present)"; return; fi
if has "$2" "$3"; then ok "$1"; else bad "$1" "$2 does not contain: $3"; fi
}
printf 'branding check (canonical: %s / "%s")\n\n' "$PKG" "$DISPLAY"
printf 'build manifests\n'
check "Cargo.toml package name" Cargo.toml "name = \"$PKG\""
check "Cargo.toml homepage" Cargo.toml "homepage = \"$HOMEPAGE\""
check "Cargo.toml description" Cargo.toml "description = \"$DESCRIPTION\""
check "bundler.toml table key" bundler.toml "[$PKG]"
check "bundler.toml display name" bundler.toml "name = \"$DISPLAY\""
printf '\nsource identity (src/lib.rs)\n'
check "Plugin::NAME" src/lib.rs "const NAME: &'static str = \"$PLUGIN_NAME\""
check "Plugin::VENDOR" src/lib.rs "const VENDOR: &'static str = \"$VENDOR\""
check "Plugin::EMAIL" src/lib.rs "const EMAIL: &'static str = \"$EMAIL\""
check "CLAP_ID" src/lib.rs "const CLAP_ID: &'static str = \"$CLAP_ID\""
# CLAP_DESCRIPTION wraps onto a second line here, so the const name and its
# value are never on one line. Match the value alone.
check "CLAP_DESCRIPTION" src/lib.rs "Some(\"$CLAP_DESCRIPTION\")"
check "VST3_CLASS_ID" src/lib.rs "const VST3_CLASS_ID: [u8; 16] = *b\"$VST3_CLASS_ID\""
# Plugin::URL is env!("CARGO_PKG_HOMEPAGE"), so the manifest is the only place
# it is spelled. Assert the indirection is still in place rather than hardcoded.
check "Plugin::URL from manifest" src/lib.rs "const URL: &'static str = env!(\"CARGO_PKG_HOMEPAGE\")"
# VST3 class IDs are a fixed 16-byte array. A shorter or longer literal is a
# compile error, but it is far clearer to say so here than to read rustc on it.
n=$(printf '%s' "$VST3_CLASS_ID" | wc -c | tr -d ' ')
if [ "$n" -eq 16 ]; then ok "VST3_CLASS_ID is 16 bytes"
else bad "VST3_CLASS_ID is 16 bytes" "got $n bytes: \"$VST3_CLASS_ID\""; fi
# CLAP IDs are reverse-DNS by convention and are compared verbatim by hosts, so
# a stray space or capital is a silent identity change between machines.
case "$CLAP_ID" in
*\ *|*[A-Z]*) bad "CLAP_ID is reverse-DNS safe" "no spaces or capitals allowed: \"$CLAP_ID\"" ;;
*.*) ok "CLAP_ID is reverse-DNS safe" ;;
*) bad "CLAP_ID is reverse-DNS safe" "expected dotted reverse-DNS, got: \"$CLAP_ID\"" ;;
esac
printf '\nstructural names (src/lib.rs)\n'
# Every site the type names appear. A rename that misses one is a compile error
# rather than silent drift, but naming the site is faster than reading rustc,
# and the Vst3Plugin impl sits behind a cfg so a default build never reports it.
check "struct $STRUCT" src/lib.rs "struct $STRUCT {"
check "impl Plugin" src/lib.rs "impl Plugin for $STRUCT {"
check "impl ClapPlugin" src/lib.rs "impl ClapPlugin for $STRUCT {"
check "impl Vst3Plugin" src/lib.rs "impl Vst3Plugin for $STRUCT {"
check "nih_export_clap!" src/lib.rs "nih_export_clap!($STRUCT);"
check "nih_export_vst3!" src/lib.rs "nih_export_vst3!($STRUCT);"
check "struct $PARAMS_STRUCT" src/params.rs "struct $PARAMS_STRUCT {"
check "impl Default for params" src/params.rs "impl Default for $PARAMS_STRUCT {"
check "params field type" src/lib.rs "params: Arc<$PARAMS_STRUCT>,"
printf '\ndocs\n'
opt_check "README bundle command" README.md "cargo xtask bundle $PKG"
printf '\ndeploy script\n'
opt_check "deploy.ps1 default package" deploy.ps1 "\$Package = \"$PKG\""
opt_check "deploy.ps1 VST3 bundle" deploy.ps1 "\$vst3Name = \"$DISPLAY.vst3\""
opt_check "deploy.ps1 CLAP bundle" deploy.ps1 "\$clapName = \"$DISPLAY.clap\""
printf '\nstale spellings\n'
# File list for the scan below. Prefer git, so build output and any vendored
# tree stay out of it. Fall back to find when this is not a checkout - an
# exported tarball, or a CI step that fetched without git metadata - because
# `git ls-files` there returns nothing, and an empty list would report every
# stale spelling as absent. A check that passes hardest when it can see least
# is worse than no check.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
file_list() { git ls-files; }
scan_source="git"
else
file_list() { find . -path ./.git -prune -o -path ./target -prune -o -type f -print | sed 's|^\./||'; }
scan_source="find (not a git checkout)"
fi
info "scanning tracked files via $scan_source"
# Every canonical spelling concatenated, for the substring test below.
CANONICAL_BLOB="$PKG
$DESCRIPTION
$CLAP_DESCRIPTION
$STRUCT
$PARAMS_STRUCT
$DISPLAY
$PLUGIN_NAME
$VENDOR
$EMAIL
$HOMEPAGE
$CLAP_ID
$VST3_CLASS_ID"
# Fed by here-doc rather than a pipe on purpose: a piped `while` runs in a
# subshell, and the pass/fail counters it increments would be discarded.
while IFS= read -r s; do
[ -n "$s" ] || continue
# Still part of a canonical value? Then it is current branding, not a leftover.
# This is what lets the list ship pre-seeded: entries arm themselves as you
# rename. Substring, not equality, because placeholders nest - "your-domain"
# lives inside the canonical CLAP_ID, and "YOUR-USERNAME" inside the homepage.
# Over-skipping is the safe direction: a false FAIL on a name you still use
# would train you to ignore this section.
case "$CANONICAL_BLOB" in
*"$s"*) note "\"$s\" (still canonical)"; continue ;;
esac
# This script names every placeholder by definition, so it can never be a hit.
hits=$(file_list 2>/dev/null | while read -r f; do
[ "$f" = "check-branding.sh" ] || { [ -f "$f" ] && grep -lF "$s" "$f" 2>/dev/null; }
done)
if [ -z "$hits" ]; then ok "no occurrences of \"$s\""
else bad "no occurrences of \"$s\"" "still present in: $(echo "$hits" | tr '\n' ' ')"; fi
done <<STALE_LIST
$STALE
STALE_LIST
# Template scaffolding, not drift: TODOs are legitimate while you are still
# building. Reported so a finished plugin does not quietly ship with them.
# `grep -c` prints 0 AND exits 1 when there are no matches, so `|| echo 0`
# would append a second zero and make the count unparseable. Swallow the
# status instead, and default an empty result (missing file) to zero.
todos=$(grep -c 'TODO' src/lib.rs 2>/dev/null || :)
[ -n "$todos" ] || todos=0
printf '\nscaffolding\n'
if [ "$todos" -eq 0 ]; then ok "no TODO markers left in src/lib.rs"
else info "$todos TODO marker(s) still in src/lib.rs (not a failure)"; fi
printf '\n%d passed, %d failed' "$pass" "$fail"
[ "$skip" -gt 0 ] && printf ', %d skipped' "$skip"
printf '\n'
[ "$fail" -eq 0 ] || exit 1
+4 -4
View File
@@ -1,6 +1,6 @@
<# <#
.SYNOPSIS .SYNOPSIS
Build Codename 206 and install the VST3 (and CLAP) into the system plugin folders. Build Bari 206 and install the VST3 (and CLAP) into the system plugin folders.
.DESCRIPTION .DESCRIPTION
FL Studio scans `C:\Program Files\Common Files\VST3` by default and ignores FL Studio scans `C:\Program Files\Common Files\VST3` by default and ignores
@@ -25,7 +25,7 @@
param( param(
[switch]$SkipBuild, [switch]$SkipBuild,
[switch]$User, [switch]$User,
[string]$Package = "codename_206" [string]$Package = "bari_206"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -42,8 +42,8 @@ if (-not $SkipBuild) {
if ($LASTEXITCODE -ne 0) { throw "cargo xtask bundle failed (exit $LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "cargo xtask bundle failed (exit $LASTEXITCODE)" }
} }
$vst3Name = "Codename 206.vst3" $vst3Name = "Bari 206.vst3"
$clapName = "Codename 206.clap" $clapName = "Bari 206.clap"
$bundled = Join-Path $root "target\bundled" $bundled = Join-Path $root "target\bundled"
$vst3Src = Join-Path $bundled $vst3Name $vst3Src = Join-Path $bundled $vst3Name
$clapSrc = Join-Path $bundled $clapName $clapSrc = Join-Path $bundled $clapName
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Generic second-order IIR biquad, Transposed Direct Form II. //! Generic second-order IIR biquad, Transposed Direct Form II.
//! //!
//! Coefficient formulas are the RBJ Audio EQ Cookbook //! Coefficient formulas are the RBJ Audio EQ Cookbook
+83 -8
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Single full-band feed-forward compressor with look-ahead. //! Single full-band feed-forward compressor with look-ahead.
//! //!
//! Design follows Giannoulis, Massberg & Reiss, "Digital Dynamic Range Compressor //! Design follows Giannoulis, Massberg & Reiss, "Digital Dynamic Range Compressor
@@ -50,7 +50,10 @@ const LOW_BULGE_MAX_DB: f32 = 12.0;
/// Hardcoded RMS averaging window (one-pole time constant). Deliberately small; can be /// Hardcoded RMS averaging window (one-pole time constant). Deliberately small; can be
/// promoted to a parameter later. /// promoted to a parameter later.
const RMS_WINDOW_MS: f32 = 5.0; /// Default RMS averaging time constant for tests that do not care which band they model.
/// Real instances pass a per-band value - see the `RMS_WINDOW_MS` table in `lib.rs`.
#[cfg(test)]
const RMS_WINDOW_DEFAULT_MS: f32 = 20.0;
// Denormals (the exponentially-decaying envelope/RMS tails and the IIR filter state) are handled // Denormals (the exponentially-decaying envelope/RMS tails and the IIR filter state) are handled
// by the CPU's Flush-To-Zero mode, which NIH-plug enables around `process()`/`reset()` via its // by the CPU's Flush-To-Zero mode, which NIH-plug enables around `process()`/`reset()` via its
@@ -125,12 +128,23 @@ impl Compressor {
/// Allocate delay buffers for the worst-case look-ahead. Call from `initialize()`, /// Allocate delay buffers for the worst-case look-ahead. Call from `initialize()`,
/// where allocation is allowed — never from `process()`. /// where allocation is allowed — never from `process()`.
pub fn prepare(&mut self, sample_rate: f32, num_channels: usize, max_lookahead_ms: f32) { /// `rms_window_ms` is the time constant of the RMS detector's one-pole average. It is a
/// design constant per band rather than a user param: the squared signal carries a component
/// at twice the fundamental, so a window that does not span a cycle leaves the detector
/// rippling at 2f and the gain intermodulating with the bass. The needed window is therefore
/// set by how low the band goes, which the band - not the compressor - knows.
pub fn prepare(
&mut self,
sample_rate: f32,
num_channels: usize,
max_lookahead_ms: f32,
rms_window_ms: f32,
) {
self.sample_rate = sample_rate; self.sample_rate = sample_rate;
self.fixed_delay = (max_lookahead_ms * 0.001 * sample_rate).ceil() as usize; self.fixed_delay = (max_lookahead_ms * 0.001 * sample_rate).ceil() as usize;
// +1 so the oldest (output) sample and the newest (write) sample never alias. // +1 so the oldest (output) sample and the newest (write) sample never alias.
self.capacity = self.fixed_delay + 1; self.capacity = self.fixed_delay + 1;
self.rms_coef = Self::time_to_coef(RMS_WINDOW_MS, sample_rate); self.rms_coef = Self::time_to_coef(rms_window_ms, sample_rate);
let channels = num_channels.clamp(1, MAX_CHANNELS); let channels = num_channels.clamp(1, MAX_CHANNELS);
self.delay = vec![vec![0.0; self.capacity]; channels]; self.delay = vec![vec![0.0; self.capacity]; channels];
self.reset(); self.reset();
@@ -379,7 +393,7 @@ mod tests {
// Constant 0.5 (-6.02 dBFS) into a -18 dB / 2:1 / hard-knee comp: // Constant 0.5 (-6.02 dBFS) into a -18 dB / 2:1 / hard-knee comp:
// reduction = -0.5 * (−6.02 − −18) = −5.99 dB, so output ≈ 0.5 * 10^(−5.99/20). // reduction = -0.5 * (−6.02 − −18) = −5.99 dB, so output ≈ 0.5 * 10^(−5.99/20).
let mut comp = Compressor::new(); let mut comp = Compressor::new();
comp.prepare(SR, 1, MAX_LOOKAHEAD_MS); comp.prepare(SR, 1, MAX_LOOKAHEAD_MS, RMS_WINDOW_DEFAULT_MS);
let set = settings(-18.0, 2.0, 0.0); let set = settings(-18.0, 2.0, 0.0);
let mut out = [0.0f32]; let mut out = [0.0f32];
for _ in 0..SR as usize { for _ in 0..SR as usize {
@@ -399,7 +413,7 @@ mod tests {
fn output_rms(use_rms: bool) -> f32 { fn output_rms(use_rms: bool) -> f32 {
let mut comp = Compressor::new(); let mut comp = Compressor::new();
comp.prepare(SR, 1, MAX_LOOKAHEAD_MS); comp.prepare(SR, 1, MAX_LOOKAHEAD_MS, RMS_WINDOW_DEFAULT_MS);
let mut set = settings(-30.0, 4.0, 0.0); let mut set = settings(-30.0, 4.0, 0.0);
set.use_rms = use_rms; set.use_rms = use_rms;
set.attack_coef = Compressor::time_to_coef(1.0, SR); set.attack_coef = Compressor::time_to_coef(1.0, SR);
@@ -433,7 +447,7 @@ mod tests {
// knob must NOT change that (this is what keeps live adjustment from renegotiating // knob must NOT change that (this is what keeps live adjustment from renegotiating
// latency and crashing the host). // latency and crashing the host).
let mut comp = Compressor::new(); let mut comp = Compressor::new();
comp.prepare(SR, 1, 0.5); // small max so the fixed delay is quick to test comp.prepare(SR, 1, 0.5, RMS_WINDOW_DEFAULT_MS); // small max so the fixed delay is quick to test
let d = comp.latency() as usize; let d = comp.latency() as usize;
assert!(d > 0); assert!(d > 0);
@@ -455,4 +469,65 @@ mod tests {
} }
} }
} }
}
/// Measure peak-to-peak gain-reduction ripple for a steady sine, with the ballistics
/// disabled so the gain follows the detector directly. Squaring puts a component at 2f in
/// the detector, so a window that does not span a cycle leaves the gain wobbling at 2f -
/// which is intermodulation, heard as growl on bass.
fn gr_ripple_db(rms_window_ms: f32, freq: f32) -> f32 {
let mut c = Compressor::new();
c.prepare(SR, 1, MAX_LOOKAHEAD_MS, rms_window_ms);
let set = CompressorSettings {
threshold_db: -40.0, ratio: 8.0, knee_db: 0.0,
// coef 0 = no attack/release smoothing, so yl == the detector's target.
attack_coef: 0.0, release_coef: 0.0,
makeup_db: 0.0, mix: 1.0, lookahead_samples: 0,
use_rms: true, low_slope: 1.0, low_curve: 0.0,
};
let mut out = [0.0f32];
// Settle well past the longest window under test.
let settle = (SR * 0.5) as usize;
for i in 0..settle {
let x = (i as f32 / SR * freq * std::f32::consts::TAU).sin();
c.process(&[x], &mut out, &set);
}
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
for i in settle..settle + (SR / freq * 4.0) as usize {
let x = (i as f32 / SR * freq * std::f32::consts::TAU).sin();
c.process(&[x], &mut out, &set);
let gr = c.gain_reduction_db();
lo = lo.min(gr);
hi = hi.max(gr);
}
hi - lo
}
/// The per-band RMS windows in `lib.rs` exist to keep this ripple down; without a test the
/// next person to "tune" them has nothing telling them what they are for.
#[test]
fn rms_window_suppresses_low_frequency_ripple() {
// The LOW band's 50 ms window against the lowest content it can be asked to carry.
let low_band = gr_ripple_db(50.0, 30.0);
assert!(low_band < 1.0, "50 ms window rippled {low_band:.2} dB at 30 Hz, expected < 1 dB");
// A short window on the same signal must be visibly worse - this is what the per-band
// table is avoiding, and it keeps the assertion above from passing for the wrong reason.
let short = gr_ripple_db(5.0, 30.0);
assert!(
short > 3.0 * low_band,
"5 ms ({short:.2} dB) should ripple far more than 50 ms ({low_band:.2} dB)"
);
// Ripple must fall monotonically as the window grows.
let mut prev = f32::MAX;
for w in [5.0, 10.0, 20.0, 35.0, 50.0] {
let r = gr_ripple_db(w, 30.0);
assert!(r < prev, "ripple did not fall from the previous window at {w} ms ({r:.2} dB)");
prev = r;
}
// The HIGH band's short window is fine because nothing up there ripples.
let high_band = gr_ripple_db(10.0, 5_000.0);
assert!(high_band < 0.1, "10 ms window rippled {high_band:.2} dB at 5 kHz");
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! 3-band Linkwitz-Riley (LR4, 24 dB/oct) crossover filterbank. //! 3-band Linkwitz-Riley (LR4, 24 dB/oct) crossover filterbank.
//! //!
//! Each crossover splits into a low-passed band output and a high-passed remainder that feeds //! Each crossover splits into a low-passed band output and a high-passed remainder that feeds
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Look-ahead brickwall peak limiter (base-rate). //! Look-ahead brickwall peak limiter (base-rate).
//! //!
//! Guarantees the output never exceeds the ceiling. A short fixed look-ahead lets the gain ramp //! Guarantees the output never exceeds the ceiling. A short fixed look-ahead lets the gain ramp
+2 -2
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! DSP building blocks for Codename 206. //! DSP building blocks for Bari 206.
//! //!
//! The signal chain: a `crossover` filterbank splits into bands, each band (plus the summed //! The signal chain: a `crossover` filterbank splits into bands, each band (plus the summed
//! 'All' channel) runs a `compressor`, and a `limiter` (with a true-peak `oversampler` detector) //! 'All' channel) runs a `compressor`, and a `limiter` (with a true-peak `oversampler` detector)
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! 4× polyphase interpolation for **true-peak (inter-sample) detection only**. //! 4× polyphase interpolation for **true-peak (inter-sample) detection only**.
//! //!
//! A band-limited signal can overshoot its sample values between samples, so the digital sample //! A band-limited signal can overshoot its sample values between samples, so the digital sample
+13 -12
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Draggable crossover handles — replaces the two plain crossover sliders. //! Draggable crossover handles — replaces the two plain crossover sliders.
//! //!
//! A horizontal log-frequency strip split into LOW / MID / HIGH by two draggable handles, plus a //! A horizontal log-frequency strip split into LOW / MID / HIGH by two draggable handles, plus a
@@ -10,7 +10,8 @@ use nih_plug_egui::egui::{
self, pos2, vec2, Align2, Color32, CornerRadius, DragValue, FontId, Painter, Rect, Sense, Stroke, self, pos2, vec2, Align2, Color32, CornerRadius, DragValue, FontId, Painter, Rect, Sense, Stroke,
}; };
use crate::params::Codename206Params; use crate::params::Bari206Params;
use super::palette;
/// Height of the handle strip. /// Height of the handle strip.
const GRAPH_H: f32 = 54.0; const GRAPH_H: f32 = 54.0;
@@ -30,7 +31,7 @@ fn fmt_hz(hz: f32) -> String {
} }
} }
pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, setter: &ParamSetter) { pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, setter: &ParamSetter) {
let lo = params.crossover_low_hz.value(); let lo = params.crossover_low_hz.value();
let hi = params.crossover_high_hz.value(); let hi = params.crossover_high_hz.value();
// Each param's own min/max (normalized 0/1 map to the range ends). // Each param's own min/max (normalized 0/1 map to the range ends).
@@ -55,19 +56,19 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, setter: &Param
let xhi = x_for(hi); let xhi = x_for(hi);
// Three band regions. // Three band regions.
p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, Color32::from_rgb(28, 38, 52)); p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, palette::CHANNELS[0].strip);
p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, Color32::from_rgb(30, 48, 36)); p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, palette::CHANNELS[1].strip);
p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, Color32::from_rgb(52, 38, 30)); p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, palette::CHANNELS[2].strip);
let band_label = |cx: f32, t: &str| { let band_label = |cx: f32, t: &str| {
p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), Color32::from_gray(160)); p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), palette::TEXT);
}; };
band_label((left + xlo) * 0.5, "LOW"); band_label((left + xlo) * 0.5, "LOW");
band_label((xlo + xhi) * 0.5, "MID"); band_label((xlo + xhi) * 0.5, "MID");
band_label((xhi + right) * 0.5, "HIGH"); band_label((xhi + right) * 0.5, "HIGH");
handle(ui, &p, setter, "xover_lo", xlo, lo, &params.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, Color32::from_rgb(120, 170, 230)); handle(ui, &p, setter, "xover_lo", xlo, lo, &params.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, palette::CHANNELS[0].handle);
handle(ui, &p, setter, "xover_hi", xhi, hi, &params.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, Color32::from_rgb(230, 150, 120)); handle(ui, &p, setter, "xover_hi", xhi, hi, &params.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, palette::CHANNELS[2].handle);
// Number boxes (double-click to type). Clamped to the same dynamic limits. // Number boxes (double-click to type). Clamped to the same dynamic limits.
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -129,9 +130,9 @@ fn handle(
setter.end_set_parameter(param); setter.end_set_parameter(param);
} }
let col = if resp.dragged() || resp.hovered() { Color32::WHITE } else { color }; let col = if resp.dragged() || resp.hovered() { palette::TEXT_HOVER } else { color };
p.line_segment([pos2(x, top), pos2(x, bottom)], Stroke::new(2.0, col)); p.line_segment([pos2(x, top), pos2(x, bottom)], Stroke::new(2.0_f32, col));
p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), Color32::from_gray(220)); p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), palette::TEXT_BRIGHT);
} }
/// Set a param to `value` clamped to `[min, max]`, wrapped in its own gesture (for the number box). /// Set a param to `value` clamped to `[min, max]`, wrapped in its own gesture (for the number box).
+17 -16
View File
@@ -1,23 +1,24 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Static gain-curve display: output level vs input level for the channel currently selected in //! Static gain-curve display: output level vs input level for the channel currently selected in
//! the plot. Plots the wet transfer `out = (in + pre_gain) + gain_reduction(...) + makeup` (mix not //! the plot. Plots the wet transfer `out = (in + pre_gain) + gain_reduction(...) + makeup` (mix not
//! folded in; output clamped at 0 dBFS), plus a live **operating-point fill** under the curve up to //! folded in; output clamped at 0 dBFS), plus a live **operating-point fill** under the curve up to
//! the channel's current input level — its right edge rides the curve (width = input, height = out). //! the channel's current input level — its right edge rides the curve (width = input, height = out).
use nih_plug::prelude::util; use nih_plug::prelude::util;
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke}; use nih_plug_egui::egui::{self, pos2, vec2, Align2, CornerRadius, FontId, Sense, Stroke};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::dsp::compressor::Compressor; use crate::dsp::compressor::Compressor;
use crate::meters::Meters; use crate::meters::Meters;
use crate::params::Codename206Params; use crate::params::Bari206Params;
use super::palette;
/// Side length of the square plot. /// Side length of the square plot.
const CURVE_SIZE: f32 = 150.0; const CURVE_SIZE: f32 = 150.0;
/// dB extent of both axes (bottom/left = FLOOR_DB, top/right = 0 dBFS). /// dB extent of both axes (bottom/left = FLOOR_DB, top/right = 0 dBFS).
const FLOOR_DB: f32 = -60.0; const FLOOR_DB: f32 = -60.0;
pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usize, meters: &Meters) { pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, meters: &Meters) {
let labels = ["LOW", "MID", "HIGH", "ALL"]; let labels = ["LOW", "MID", "HIGH", "ALL"];
let ch = selected.min(3); let ch = selected.min(3);
let cp = match ch { let cp = match ch {
@@ -37,7 +38,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
ui.label(format!("Curve: {}", labels[ch])); ui.label(format!("Curve: {}", labels[ch]));
let (rect, _) = ui.allocate_exact_size(vec2(CURVE_SIZE, CURVE_SIZE), Sense::hover()); let (rect, _) = ui.allocate_exact_size(vec2(CURVE_SIZE, CURVE_SIZE), Sense::hover());
let p = ui.painter_at(rect); let p = ui.painter_at(rect);
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20)); p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE);
let inset = 2.0; let inset = 2.0;
let (left, right, top, bottom) = let (left, right, top, bottom) =
@@ -48,17 +49,17 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h; let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h;
// -6 dBFS reference lines on both axes. // -6 dBFS reference lines on both axes.
let g6 = Color32::from_gray(38); let g6 = palette::GRID_FAINT;
let x6 = x_for(-6.0); let x6 = x_for(-6.0);
let y6 = y_for(-6.0); let y6 = y_for(-6.0);
p.line_segment([pos2(x6, top), pos2(x6, bottom)], Stroke::new(1.0, g6)); p.line_segment([pos2(x6, top), pos2(x6, bottom)], Stroke::new(1.0_f32, g6));
p.line_segment([pos2(left, y6), pos2(right, y6)], Stroke::new(1.0, g6)); p.line_segment([pos2(left, y6), pos2(right, y6)], Stroke::new(1.0_f32, g6));
// Unity reference (out = in), bottom-left to top-right. // Unity reference (out = in), bottom-left to top-right.
p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, Color32::from_gray(45))); p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0_f32, palette::GRID));
// Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre). // Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre).
let tx = x_for((threshold - pre).clamp(FLOOR_DB, 0.0)); let tx = x_for((threshold - pre).clamp(FLOOR_DB, 0.0));
p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, Color32::from_rgb(80, 60, 45))); p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0_f32, palette::THRESHOLD_LINE));
// Full wet transfer: drive into the comp, then makeup. (Mix not folded in.) // Full wet transfer: drive into the comp, then makeup. (Mix not folded in.)
let n = 96; let n = 96;
@@ -74,7 +75,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
let driven_now = util::gain_to_db(meters.input_level[ch].load(Ordering::Relaxed)); let driven_now = util::gain_to_db(meters.input_level[ch].load(Ordering::Relaxed));
let ext_in = (driven_now - pre).clamp(FLOOR_DB, 0.0); // external input -> curve x let ext_in = (driven_now - pre).clamp(FLOOR_DB, 0.0); // external input -> curve x
let x_now = x_for(ext_in); let x_now = x_for(ext_in);
let fill_col = Color32::from_rgba_unmultiplied(120, 200, 160, 45); let fill_col = palette::with_alpha(palette::CURVE, 45);
for seg in pts.windows(2) { for seg in pts.windows(2) {
let a = seg[0]; let a = seg[0];
let mut b = seg[1]; let mut b = seg[1];
@@ -92,16 +93,16 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
)); ));
} }
p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160)))); p.add(egui::Shape::line(pts, Stroke::new(1.6_f32, palette::CURVE)));
// Operating-point dot, on the curve at the current input. // Operating-point dot, on the curve at the current input.
let driven = ext_in + pre; let driven = ext_in + pre;
let gr = Compressor::gain_computer(driven, threshold, ratio, knee, low_slope, low_curve); let gr = Compressor::gain_computer(driven, threshold, ratio, knee, low_slope, low_curve);
let out_op = (driven + gr + makeup).clamp(FLOOR_DB, 0.0); let out_op = (driven + gr + makeup).clamp(FLOOR_DB, 0.0);
p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, Color32::from_rgb(235, 240, 235)); p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, palette::OPERATING_POINT);
// Corner dB ticks + the -6 dB reference. // Corner dB ticks + the -6 dB reference.
p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), Color32::from_gray(90)); p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), palette::TEXT_DIM);
p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), Color32::from_gray(90)); p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), palette::TEXT_DIM);
p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), Color32::from_gray(80)); p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), palette::TEXT_DIM);
} }
+26 -20
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Per-channel level + gain-reduction meters, each with its own latching ceiling/over lamp. //! Per-channel level + gain-reduction meters, each with its own latching ceiling/over lamp.
//! //!
//! Each channel is a `|L | GR | R|` cluster (output level left/right, mono gain reduction in the //! Each channel is a `|L | GR | R|` cluster (output level left/right, mono gain reduction in the
@@ -15,6 +15,7 @@ use std::sync::atomic::Ordering;
use super::METER_FLOOR_DB; use super::METER_FLOOR_DB;
use crate::meters::{Meters, NUM_CHANNELS}; use crate::meters::{Meters, NUM_CHANNELS};
use super::palette;
/// Full-scale of the gain-reduction bar (fills downward from the top). /// Full-scale of the gain-reduction bar (fills downward from the top).
const GR_FULL_DB: f32 = 24.0; const GR_FULL_DB: f32 = 24.0;
@@ -47,7 +48,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
let (rect, _) = let (rect, _) =
ui.allocate_exact_size(vec2(ui.available_width(), METER_PANEL_H), Sense::hover()); ui.allocate_exact_size(vec2(ui.available_width(), METER_PANEL_H), Sense::hover());
let p = ui.painter_at(rect); let p = ui.painter_at(rect);
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(20, 20, 24)); p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE_RAISED);
let top = rect.top() + 22.0; // leave a row at the top for the lamps let top = rect.top() + 22.0; // leave a row at the top for the lamps
let bottom = rect.bottom() - 18.0; // and a row at the bottom for the labels let bottom = rect.bottom() - 18.0; // and a row at the bottom for the labels
@@ -58,6 +59,22 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
for i in 0..NUM_CHANNELS { for i in 0..NUM_CHANNELS {
let cell_left = rect.left() + i as f32 * cell_w; let cell_left = rect.left() + i as f32 * cell_w;
// Channel chrome. The four clusters are otherwise identical and told apart only by the
// label underneath, which is the one place channel identity is hardest to read. A dark
// wash behind the cell plus an accent rule under the bars fixes that without touching
// the bars themselves - see `palette`: hue is chrome, functional colour is data.
let cell = Rect::from_min_max(
pos2(cell_left + 1.0, rect.top()),
pos2(cell_left + cell_w - 1.0, rect.bottom()),
);
p.rect_filled(cell, CornerRadius::ZERO, palette::CHANNELS[i].chrome);
p.rect_filled(
Rect::from_min_max(pos2(cell.left(), bottom + 2.0), pos2(cell.right(), bottom + 4.0)),
CornerRadius::ZERO,
palette::CHANNELS[i].edge,
);
let group_w = bar_w * 3.0 + gap * 2.0; let group_w = bar_w * 3.0 + gap * 2.0;
let bx = cell_left + (cell_w - group_w) * 0.5; let bx = cell_left + (cell_w - group_w) * 0.5;
@@ -71,16 +88,16 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
let gr_db = meters.gain_reduction_db[i].load(Ordering::Relaxed); let gr_db = meters.gain_reduction_db[i].load(Ordering::Relaxed);
let gr_frac = (gr_db / GR_FULL_DB).clamp(0.0, 1.0); let gr_frac = (gr_db / GR_FULL_DB).clamp(0.0, 1.0);
v_bar(&p, bx, bar_w, top, bottom, l_frac, level_color(l_db), false); v_bar(&p, bx, bar_w, top, bottom, l_frac, palette::level_color(l_db), false);
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true); v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, palette::SIGNAL_GR, true);
v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, level_color(r_db), false); v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, palette::level_color(r_db), false);
p.text( p.text(
pos2(cell_left + cell_w * 0.5, rect.bottom() - 2.0), pos2(cell_left + cell_w * 0.5, rect.bottom() - 2.0),
Align2::CENTER_BOTTOM, Align2::CENTER_BOTTOM,
labels[i], labels[i],
FontId::proportional(12.0), FontId::proportional(12.0),
Color32::from_gray(200), palette::TEXT_BRIGHT,
); );
// Per-channel lamp: latch on output reaching 0 dBFS; the ALL channel also latches when the // Per-channel lamp: latch on output reaching 0 dBFS; the ALL channel also latches when the
@@ -109,9 +126,9 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
} }
} }
let lamp = if state.ceiling_trigger[i].is_some() { let lamp = if state.ceiling_trigger[i].is_some() {
Color32::from_rgb(255, 40, 40) palette::LAMP_ON
} else { } else {
Color32::from_rgb(40, 12, 12) palette::LAMP_OFF
}; };
p.circle_filled(lamp_center, 5.0, lamp); p.circle_filled(lamp_center, 5.0, lamp);
} }
@@ -120,7 +137,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
/// Draw a vertical bar within `[top, bottom]`. `frac` is 0..1; `from_top` fills downward from the /// Draw a vertical bar within `[top, bottom]`. `frac` is 0..1; `from_top` fills downward from the
/// top (gain reduction) instead of upward from the bottom (level). /// top (gain reduction) instead of upward from the bottom (level).
fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Color32, from_top: bool) { fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Color32, from_top: bool) {
let track = Color32::from_rgb(34, 34, 40); let track = palette::TRACK;
p.rect_filled(Rect::from_min_max(pos2(x, top), pos2(x + w, bottom)), CornerRadius::ZERO, track); p.rect_filled(Rect::from_min_max(pos2(x, top), pos2(x + w, bottom)), CornerRadius::ZERO, track);
let h = (bottom - top) * frac.clamp(0.0, 1.0); let h = (bottom - top) * frac.clamp(0.0, 1.0);
@@ -131,14 +148,3 @@ fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Co
}; };
p.rect_filled(filled, CornerRadius::ZERO, fill); p.rect_filled(filled, CornerRadius::ZERO, fill);
} }
/// Level-bar colour: green below -6 dB, yellow approaching, red near 0 dBFS.
fn level_color(db: f32) -> Color32 {
if db >= -1.0 {
Color32::from_rgb(235, 70, 60)
} else if db >= -6.0 {
Color32::from_rgb(230, 200, 70)
} else {
Color32::from_rgb(90, 200, 110)
}
}
+7 -6
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! egui editor: assembly + control layout. //! egui editor: assembly + control layout.
//! //!
//! The aggregator. Builds the editor window and lays out the heading, the meter panel //! The aggregator. Builds the editor window and lays out the heading, the meter panel
@@ -17,12 +17,13 @@ use nih_plug_egui::{
use std::sync::Arc; use std::sync::Arc;
use crate::meters::Meters; use crate::meters::Meters;
use crate::params::{Codename206Params, CompressorParams}; use crate::params::{Bari206Params, CompressorParams};
use crate::Codename206; use crate::Bari206;
mod crossover; mod crossover;
mod gain_curve; mod gain_curve;
mod meter; mod meter;
mod palette;
mod plot; mod plot;
/// Bottom of the dB scale shared by the meters and the plot (top is 0 dBFS). /// Bottom of the dB scale shared by the meters and the plot (top is 0 dBFS).
@@ -36,12 +37,12 @@ struct EditorState {
} }
/// Build the plugin editor over shared handles to the params and meter state. /// Build the plugin editor over shared handles to the params and meter state.
pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Option<Box<dyn Editor>> { pub(crate) fn create(params: Arc<Bari206Params>, meters: Arc<Meters>) -> Option<Box<dyn Editor>> {
let egui_state = params.editor_state.clone(); let egui_state = params.editor_state.clone();
create_egui_editor( create_egui_editor(
params.editor_state.clone(), params.editor_state.clone(),
EditorState::default(), EditorState::default(),
|_, _| {}, |egui_ctx, _| palette::apply_visuals(egui_ctx),
move |egui_ctx, setter, state| { move |egui_ctx, setter, state| {
// Keep frames coming so the meters animate and the lamp can time out while open. // Keep frames coming so the meters animate and the lamp can time out while open.
egui_ctx.request_repaint(); egui_ctx.request_repaint();
@@ -79,7 +80,7 @@ pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Opt
.min_size(Vec2::new(480.0, 320.0)) .min_size(Vec2::new(480.0, 320.0))
.show(egui_ctx, egui_state.as_ref(), |ui| { .show(egui_ctx, egui_state.as_ref(), |ui| {
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.heading(Codename206::NAME); ui.heading(Bari206::NAME);
meter::draw(ui, &meters, &mut state.meter); meter::draw(ui, &meters, &mut state.meter);
ui.separator(); ui.separator();
// Gain curve (left, square) beside the scrolling plot (right, fills the rest). // Gain curve (left, square) beside the scrolling plot (right, fills the rest).
+265
View File
@@ -0,0 +1,265 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//! The plugin's colour system.
//!
//! # The rule
//!
//! **Channel hue is chrome; functional colour is data.** Before picking a colour, ask what the
//! pixel is for. Is it showing a *value* — a level, a reduction, a clip? Then it comes from the
//! functional palette, and it means the same thing in every channel. Is it telling you *which
//! channel you are looking at*? Then it comes from that channel's hue, and it never touches the
//! data itself.
//!
//! This is what keeps the two systems from fighting. An earlier version of this UI used blue,
//! green and orange for LOW/MID/HIGH in the crossover *and* for input/output/gain-reduction in
//! the scope, so orange meant "the high band" in one widget and "gain reduction" in the one next
//! to it.
//!
//! # The two tiers
//!
//! 1. **Seeds** — the short, hand-picked list in the first half of this file. This is the part
//! you edit. Everything else is derived from it.
//! 2. **Derived** — [`Channel`] palettes built from a hue by [`channel`]. Change a hue and every
//! tint, edge and strip using it moves with it.
//!
//! All derivation is `const fn`, so the whole table is computed at compile time and costs nothing
//! at runtime. That is also why it uses plain HSV rather than `ecolor::Hsva`, whose conversion is
//! not `const`.
//!
//! # Editing this file
//!
//! * To recolour a channel, change one `HUE_*` constant. Never hand-edit a derived value.
//! * To retune the meter's headroom ramp, edit [`LEVEL_OK`] / [`LEVEL_WARN`] / [`LEVEL_HOT`] and
//! the thresholds in [`level_color`]. Both are deliberately literal rather than interpolated,
//! so you can set each one by eye without fighting a gradient.
//! * To change how loudly channel identity reads, edit the `(saturation, value)` pairs in
//! [`channel`].
//! * Anything new belongs here and must be reachable from a seed — a colour written at a call
//! site is the next magic number.
use nih_plug_egui::egui::Color32;
// ===========================================================================
// TIER 1 — SEEDS. This is the part to edit.
// ===========================================================================
// --- Channel identity ------------------------------------------------------
// Hues in degrees: 0 = red, 120 = green, 240 = blue. Only the hue is chosen here; saturation and
// brightness are decided per role by `channel()`, so a channel reads as the same colour whether
// it is a 12%-brightness background wash or a full-strength drag handle.
//
// Keep neighbours far enough apart to survive being dimmed — most chrome lives at low
// saturation, where close hues merge. Roughly 60–90 degrees apart works; below ~40 does not.
pub const HUE_LOW: f32 = 233.0; // blue
pub const HUE_MID: f32 = 20.0; // orange
pub const HUE_HIGH: f32 = 155.0; // blueish green
pub const HUE_ALL: f32 = 280.0; // violet — the aggregate, distinct from its three inputs
// --- Functional colours ----------------------------------------------------
// Outside the hue system on purpose: these mean the same thing in every channel, so they must
// NOT be derived from a channel hue. Red is always "you have hit a limit".
/// Meter bar with comfortable headroom.
pub const LEVEL_OK: Color32 = Color32::from_rgb(90, 200, 110);
/// Meter bar approaching full scale.
pub const LEVEL_WARN: Color32 = Color32::from_rgb(230, 200, 70);
/// Meter bar at or near full scale.
pub const LEVEL_HOT: Color32 = Color32::from_rgb(235, 70, 60);
/// Latched over/ceiling lamp, lit.
pub const LAMP_ON: Color32 = Color32::from_rgb(255, 40, 40);
/// The same lamp unlit — dark red rather than grey, so it reads as "off", not "absent".
pub const LAMP_OFF: Color32 = Color32::from_rgb(40, 12, 12);
/// Ceiling-hit tick marks along the top edge of the scope.
pub const CLIP: Color32 = Color32::from_rgb(235, 45, 45);
// Scope traces. The scope is tabbed — one channel at a time — so these never compete with a
// channel hue: the tab chrome says which channel, these three say what each line is.
pub const SIGNAL_IN: Color32 = Color32::from_rgb(90, 170, 235);
pub const SIGNAL_OUT: Color32 = Color32::from_rgb(90, 200, 110);
pub const SIGNAL_GR: Color32 = Color32::from_rgb(240, 150, 60);
/// The transfer curve in the gain-curve display, and its translucent fill.
pub const CURVE: Color32 = Color32::from_rgb(120, 200, 160);
/// The live operating-point dot riding that curve — near-white, so it reads on top of the fill.
pub const OPERATING_POINT: Color32 = Color32::from_rgb(235, 240, 235);
/// Threshold marker on the gain curve. Dim on purpose: a reference line, not data.
pub const THRESHOLD_LINE: Color32 = Color32::from_rgb(80, 60, 45);
// --- Neutrals --------------------------------------------------------------
// Eight ad-hoc greys collapsed to six named steps. If you need another, add it here rather than
// reaching for `from_gray` at a call site.
/// Data panels — the scope and the gain curve.
pub const SURFACE: Color32 = Color32::from_rgb(16, 16, 20);
/// The meter panel, one step lighter so the cluster reads as its own surface. These were two
/// different blacks by accident before; they are two on purpose now. Set this equal to
/// [`SURFACE`] if you would rather they sat flush.
pub const SURFACE_RAISED: Color32 = Color32::from_rgb(20, 20, 24);
/// The empty part of a meter bar.
pub const TRACK: Color32 = Color32::from_rgb(34, 34, 40);
/// Structural lines — the unity diagonal, panel edges.
pub const GRID: Color32 = Color32::from_gray(45);
/// Reference lines that should recede — dB gridlines, −6 dB marks.
pub const GRID_FAINT: Color32 = Color32::from_gray(38);
/// Axis numbers and other text that must not compete with the data.
pub const TEXT_DIM: Color32 = Color32::from_gray(90);
/// Body text — band names on the crossover strip.
pub const TEXT: Color32 = Color32::from_gray(160);
/// Emphasised text — channel labels, readouts.
pub const TEXT_BRIGHT: Color32 = Color32::from_gray(210);
/// Hover and drag feedback, where the point is to be unmistakable.
pub const TEXT_HOVER: Color32 = Color32::WHITE;
// ===========================================================================
// TIER 2 — DERIVED. Generated from the seeds above; do not hand-edit.
// ===========================================================================
/// One channel's colours, all derived from a single hue.
pub struct Channel {
/// Background wash behind a channel's cell. Very dark — it must associate the bars drawn on
/// top of it with a channel without competing with them.
pub chrome: Color32,
/// Band strip on the crossover widget: between [`Channel::chrome`] and [`Channel::edge`].
pub strip: Color32,
/// Accent rule under a meter cell, and the scope's tab underline. Bright enough to identify
/// at a glance.
pub edge: Color32,
/// Full strength — drag handles and anything else the user grabs.
pub handle: Color32,
}
/// Build a channel's palette from its hue.
///
/// The `(saturation, value)` pairs below decide how loudly channel identity reads. Raise
/// `CHROME`'s value and the cell washes get more obvious; lower it and they recede into the
/// panel background. `CHROME` is deliberately near the floor: it sits *behind* meter bars.
pub const fn channel(hue: f32) -> Channel {
const CHROME: (f32, f32) = (0.45, 0.115);
const STRIP: (f32, f32) = (0.42, 0.20);
const EDGE: (f32, f32) = (0.60, 0.55);
const HANDLE: (f32, f32) = (0.55, 0.90);
Channel {
chrome: hsv(hue, CHROME.0, CHROME.1),
strip: hsv(hue, STRIP.0, STRIP.1),
edge: hsv(hue, EDGE.0, EDGE.1),
handle: hsv(hue, HANDLE.0, HANDLE.1),
}
}
/// Indexed by the `LOW`/`MID`/`HIGH`/`ALL` consts in `lib.rs`, so `CHANNELS[i]` lines up with
/// `meters.level_l[i]` and friends without a lookup step.
pub const CHANNELS: [Channel; 4] = [
channel(HUE_LOW),
channel(HUE_MID),
channel(HUE_HIGH),
channel(HUE_ALL),
];
/// HSV to RGB, `const` so [`CHANNELS`] is built at compile time.
///
/// `h` in degrees (wrapped), `s` and `v` in `0..=1`. The standard sextant algorithm, written out
/// here only because `ecolor::Hsva`'s conversion is not a `const fn`.
pub const fn hsv(h: f32, s: f32, v: f32) -> Color32 {
// Wrap into 0..360. A loop rather than `%` because float remainder is not available in a
// const context on this toolchain; hues are small numbers, so this costs nothing.
let mut h = h;
while h < 0.0 {
h += 360.0;
}
while h >= 360.0 {
h -= 360.0;
}
let sector = h / 60.0;
let i = sector as i32;
let f = sector - i as f32;
let p = v * (1.0 - s);
let q = v * (1.0 - s * f);
let t = v * (1.0 - s * (1.0 - f));
let (r, g, b) = match i {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
_ => (v, p, q),
};
Color32::from_rgb(to_u8(r), to_u8(g), to_u8(b))
}
/// Clamping float-to-byte, `const` for the same reason as [`hsv`].
const fn to_u8(x: f32) -> u8 {
let v = x * 255.0 + 0.5;
if v <= 0.0 {
0
} else if v >= 255.0 {
255
} else {
v as u8
}
}
/// Meter bar colour for an output level in dB.
///
/// Functional, not per-channel: −6 dB means the same thing in every band. Thresholds and colours
/// are both literal so either can be tuned by eye.
pub fn level_color(db: f32) -> Color32 {
if db >= -1.0 {
LEVEL_HOT
} else if db >= -6.0 {
LEVEL_WARN
} else {
LEVEL_OK
}
}
/// The same colour at `a`/255 opacity, for translucent fills under a trace.
///
/// Not `const`: `Color32::from_rgba_unmultiplied` is not a `const fn`, and the premultiplied
/// constructor that is would push the multiply out to every call site.
pub fn with_alpha(c: Color32, a: u8) -> Color32 {
Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), a)
}
/// Apply the palette to egui's own widgets, so `ParamSlider`, labels and separators sit in the
/// same world as the hand-painted ones instead of on stock egui defaults.
///
/// Call once when the editor is created. Widget colours are per interaction state: `weak_bg_fill`
/// is an unfilled slider track, `bg_fill` the filled portion. Interactive states borrow the ALL
/// channel's hue, since egui widgets are not per-band — change that to a dedicated accent seed if
/// you would rather they were neutral.
pub fn apply_visuals(ctx: &nih_plug_egui::egui::Context) {
use nih_plug_egui::egui::{Stroke, Visuals};
let accent = &CHANNELS[3];
let mut v = Visuals::dark();
v.panel_fill = SURFACE;
v.window_fill = SURFACE;
v.extreme_bg_color = SURFACE;
v.faint_bg_color = SURFACE_RAISED;
let w = &mut v.widgets;
w.noninteractive.bg_fill = SURFACE_RAISED;
w.noninteractive.weak_bg_fill = SURFACE_RAISED;
w.noninteractive.bg_stroke = Stroke::new(1.0_f32, GRID_FAINT);
w.noninteractive.fg_stroke = Stroke::new(1.0_f32, TEXT);
w.inactive.bg_fill = TRACK;
w.inactive.weak_bg_fill = TRACK;
w.inactive.bg_stroke = Stroke::new(1.0_f32, GRID);
w.inactive.fg_stroke = Stroke::new(1.0_f32, TEXT_BRIGHT);
w.hovered.bg_fill = accent.edge;
w.hovered.weak_bg_fill = accent.strip;
w.hovered.bg_stroke = Stroke::new(1.0_f32, accent.edge);
w.hovered.fg_stroke = Stroke::new(1.0_f32, TEXT_HOVER);
w.active.bg_fill = accent.handle;
w.active.weak_bg_fill = accent.edge;
w.active.bg_stroke = Stroke::new(1.0_f32, accent.handle);
w.active.fg_stroke = Stroke::new(1.0_f32, TEXT_HOVER);
ctx.set_visuals(v);
}
+11 -10
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Rolling input/output/gain-reduction plot with per-channel tabs and a flow-speed selector. //! Rolling input/output/gain-reduction plot with per-channel tabs and a flow-speed selector.
//! //!
//! Histories for all four channels run continuously (cheap), so switching tabs shows that //! Histories for all four channels run continuously (cheap), so switching tabs shows that
@@ -12,15 +12,16 @@ use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontI
use super::METER_FLOOR_DB; use super::METER_FLOOR_DB;
use crate::meters::{Meters, BUCKET_HZ, NUM_CHANNELS}; use crate::meters::{Meters, BUCKET_HZ, NUM_CHANNELS};
use super::palette;
/// Height of the plot panel. /// Height of the plot panel.
const PLOT_PANEL_H: f32 = 150.0; const PLOT_PANEL_H: f32 = 150.0;
/// Number of columns held in the history ring. /// Number of columns held in the history ring.
const PLOT_N: usize = 256; const PLOT_N: usize = 256;
const COLOR_IN: Color32 = Color32::from_rgb(90, 170, 235); const COLOR_IN: Color32 = palette::SIGNAL_IN;
const COLOR_OUT: Color32 = Color32::from_rgb(90, 200, 110); const COLOR_OUT: Color32 = palette::SIGNAL_OUT;
const COLOR_GR: Color32 = Color32::from_rgb(240, 150, 60); const COLOR_GR: Color32 = palette::SIGNAL_GR;
/// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns. /// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns.
struct PlotHistory { struct PlotHistory {
@@ -156,7 +157,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
let (rect, _) = let (rect, _) =
ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover()); ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover());
let p = ui.painter_at(rect); let p = ui.painter_at(rect);
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20)); p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE);
let (top, bottom, left, right) = let (top, bottom, left, right) =
(rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0); (rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0);
@@ -169,13 +170,13 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
// Gridlines (dB). // Gridlines (dB).
for &g in &[0.0f32, -12.0, -24.0, -48.0] { for &g in &[0.0f32, -12.0, -24.0, -48.0] {
let y = y_for_db(g); let y = y_for_db(g);
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, Color32::from_gray(40))); p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0_f32, palette::GRID_FAINT));
p.text( p.text(
pos2(left + 2.0, y), pos2(left + 2.0, y),
Align2::LEFT_BOTTOM, Align2::LEFT_BOTTOM,
format!("{g:.0}"), format!("{g:.0}"),
FontId::proportional(9.0), FontId::proportional(9.0),
Color32::from_gray(90), palette::TEXT_DIM,
); );
} }
@@ -193,7 +194,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
// Fill as a strip of per-segment convex quads down to the baseline. A single // Fill as a strip of per-segment convex quads down to the baseline. A single
// concave polygon mis-tessellates in egui (it fans from one corner, leaving stray // concave polygon mis-tessellates in egui (it fans from one corner, leaving stray
// triangles), so build convex pieces — one box per time unit — instead. // triangles), so build convex pieces — one box per time unit — instead.
let fill_col = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 40); let fill_col = palette::with_alpha(color, 40);
for seg in pts.windows(2) { for seg in pts.windows(2) {
let (a, b) = (seg[0], seg[1]); let (a, b) = (seg[0], seg[1]);
p.add(egui::Shape::convex_polygon( p.add(egui::Shape::convex_polygon(
@@ -203,7 +204,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
)); ));
} }
} }
p.add(egui::Shape::line(pts, Stroke::new(1.5, color))); p.add(egui::Shape::line(pts, Stroke::new(1.5_f32, color)));
}; };
draw_series(&state.history.in_db[c], &|db| db, COLOR_IN, true); draw_series(&state.history.in_db[c], &|db| db, COLOR_IN, true);
draw_series(&state.history.out_db[c], &|db| db, COLOR_OUT, true); draw_series(&state.history.out_db[c], &|db| db, COLOR_OUT, true);
@@ -214,7 +215,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
// hit the ceiling (global — shown on every channel's view). One column wide, so runs of // hit the ceiling (global — shown on every channel's view). One column wide, so runs of
// hits merge into a continuous segment and a lone hit is just a dot. Nothing otherwise. // hits merge into a continuous segment and a lone hit is just a dot. Nothing otherwise.
let dx = width / (PLOT_N - 1) as f32; let dx = width / (PLOT_N - 1) as f32;
let marker = Color32::from_rgb(235, 45, 45); let marker = palette::CLIP;
for k in 0..len { for k in 0..len {
let idx = (write + PLOT_N - len + k) % PLOT_N; let idx = (write + PLOT_N - len + k) % PLOT_N;
if state.history.hit[idx] { if state.history.hit[idx] {
+33 -18
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
use nih_plug::prelude::*; use nih_plug::prelude::*;
use std::sync::Arc; use std::sync::Arc;
@@ -11,7 +11,7 @@ use dsp::compressor::{Compressor, MAX_LOOKAHEAD_MS};
use dsp::crossover::Crossover; use dsp::crossover::Crossover;
use dsp::limiter::Limiter; use dsp::limiter::Limiter;
use meters::Meters; use meters::Meters;
use params::{build_settings, Codename206Params}; use params::{build_settings, Bari206Params};
/// Peak-meter fall: after this long of silence the bars decay by 12 dB. (Matches nih-plug's /// Peak-meter fall: after this long of silence the bars decay by 12 dB. (Matches nih-plug's
/// gain-gui example feel.) /// gain-gui example feel.)
@@ -23,13 +23,28 @@ const MID: usize = 1;
const HIGH: usize = 2; const HIGH: usize = 2;
const ALL: usize = 3; const ALL: usize = 3;
/// Codename 206 — Stage 3: 3-band crossover + per-band compressors summed into an 'All' channel. /// RMS detector time constants, one per channel, indexed by the consts above.
///
/// Squaring the detector signal puts a component at twice the fundamental, so a one-pole that
/// does not span a cycle leaves the detector rippling and the gain intermodulating with the
/// bass. Measured peak-to-peak detector ripple at a 5 ms window was 4.4 dB at 30 Hz and 2.7 dB
/// at 50 Hz - squarely in the low band, whose crossover reaches down to 30 Hz. The cost of a
/// longer window is response time (95% of a step: ~12 ms at 5 ms, ~117 ms at 50 ms), so each
/// band gets only as much as its frequency range actually needs:
///
/// * LOW - 50 ms: 30 Hz ripple drops to 0.46 dB, and slow is the wanted behaviour down here.
/// * MID - 20 ms: enough for body without dulling the band that carries most transients.
/// * HIGH - 10 ms: nothing up here ripples, so keep it responsive.
/// * ALL - 35 ms: full-band, so it must cover the low content too.
const RMS_WINDOW_MS: [f32; 4] = [50.0, 20.0, 10.0, 35.0];
/// Bari 206 — Stage 3: 3-band crossover + per-band compressors summed into an 'All' channel.
/// ///
/// Signal: input → LR4 crossover → {low, mid, high} each through their own compressor → sum → /// Signal: input → LR4 crossover → {low, mid, high} each through their own compressor → sum →
/// 'All' compressor → output. Bypassing low+mid+high collapses it to a plain full-band comp /// 'All' compressor → output. Bypassing low+mid+high collapses it to a plain full-band comp
/// driven by the 'All' channel (the crossover sums flat). /// driven by the 'All' channel (the crossover sums flat).
struct Codename206 { struct Bari206 {
params: Arc<Codename206Params>, params: Arc<Bari206Params>,
sample_rate: f32, sample_rate: f32,
crossover: Crossover, crossover: Crossover,
/// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`]. /// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`].
@@ -57,10 +72,10 @@ struct Codename206 {
/// Limiter gain reduction (dB) above which a plot bucket is flagged as hitting the ceiling. /// Limiter gain reduction (dB) above which a plot bucket is flagged as hitting the ceiling.
const CEILING_HIT_GR_DB: f32 = 0.1; const CEILING_HIT_GR_DB: f32 = 0.1;
impl Default for Codename206 { impl Default for Bari206 {
fn default() -> Self { fn default() -> Self {
Self { Self {
params: Arc::new(Codename206Params::default()), params: Arc::new(Bari206Params::default()),
sample_rate: 48_000.0, sample_rate: 48_000.0,
crossover: Crossover::new(), crossover: Crossover::new(),
comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()], comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()],
@@ -77,14 +92,14 @@ impl Default for Codename206 {
} }
} }
impl Codename206 { impl Bari206 {
fn lookahead_samples(&self) -> usize { fn lookahead_samples(&self) -> usize {
(self.params.look_ahead_ms.value() * 0.001 * self.sample_rate).round() as usize (self.params.look_ahead_ms.value() * 0.001 * self.sample_rate).round() as usize
} }
} }
impl Plugin for Codename206 { impl Plugin for Bari206 {
const NAME: &'static str = "206 prototype"; const NAME: &'static str = "Bari 206";
const VENDOR: &'static str = "Novoyuuparosk"; const VENDOR: &'static str = "Novoyuuparosk";
const URL: &'static str = env!("CARGO_PKG_HOMEPAGE"); const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
const EMAIL: &'static str = "mikkeli@novoyuuparosk.org"; const EMAIL: &'static str = "mikkeli@novoyuuparosk.org";
@@ -140,8 +155,8 @@ impl Plugin for Codename206 {
self.scope_bucket_len = self.scope_bucket_len =
((self.sample_rate / meters::BUCKET_HZ as f32).round() as usize).max(1); ((self.sample_rate / meters::BUCKET_HZ as f32).round() as usize).max(1);
for comp in &mut self.comps { for (i, comp) in self.comps.iter_mut().enumerate() {
comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS); comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS, RMS_WINDOW_MS[i]);
} }
self.crossover.prepare(channels); self.crossover.prepare(channels);
self.crossover.update( self.crossover.update(
@@ -388,8 +403,8 @@ impl Plugin for Codename206 {
} }
} }
impl ClapPlugin for Codename206 { impl ClapPlugin for Bari206 {
const CLAP_ID: &'static str = "com.mikkeli.codename-206"; const CLAP_ID: &'static str = "org.novoyuuparosk.bari-206";
const CLAP_DESCRIPTION: Option<&'static str> = const CLAP_DESCRIPTION: Option<&'static str> =
Some("Multiband compressor/limiter (stage 3: 3-band + 'All' channel)"); Some("Multiband compressor/limiter (stage 3: 3-band + 'All' channel)");
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
@@ -403,11 +418,11 @@ impl ClapPlugin for Codename206 {
]; ];
} }
impl Vst3Plugin for Codename206 { impl Vst3Plugin for Bari206 {
const VST3_CLASS_ID: [u8; 16] = *b"Codename206Maxi!"; const VST3_CLASS_ID: [u8; 16] = *b"Bari206Multiband";
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics]; &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
} }
nih_export_clap!(Codename206); nih_export_clap!(Bari206);
nih_export_vst3!(Codename206); nih_export_vst3!(Bari206);
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Lock-free meter state shared from the audio thread to the editor. //! Lock-free meter state shared from the audio thread to the editor.
//! //!
//! Two feeds, both written by `process()` (single producer) and read by the editor (single //! Two feeds, both written by `process()` (single producer) and read by the editor (single
+3 -3
View File
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: GPL-3.0-or-later
//! Plugin parameters and their layout. //! Plugin parameters and their layout.
//! //!
//! Holds the global controls plus four `CompressorParams` blocks (low/mid/high + the 'All' //! Holds the global controls plus four `CompressorParams` blocks (low/mid/high + the 'All'
@@ -23,7 +23,7 @@ pub enum DetectionMode {
} }
#[derive(Params)] #[derive(Params)]
pub struct Codename206Params { pub struct Bari206Params {
#[persist = "editor-state"] #[persist = "editor-state"]
pub editor_state: Arc<EguiState>, pub editor_state: Arc<EguiState>,
@@ -87,7 +87,7 @@ pub struct CompressorParams {
pub mix: FloatParam, pub mix: FloatParam,
} }
impl Default for Codename206Params { impl Default for Bari206Params {
fn default() -> Self { fn default() -> Self {
Self { Self {
editor_state: EguiState::from_size(760, 520), editor_state: EguiState::from_size(760, 520),
+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
+29
View File
@@ -0,0 +1,29 @@
req pkg.cargo Cargo.toml name = "bari_206"
req pkg.bundler bundler.toml [bari_206]
opt pkg.lock Cargo.lock name = "bari_206"
opt pkg.ci .github/workflows/ci.yaml cargo xtask bundle bari_206
opt pkg.readme README.md cargo xtask bundle bari_206
req display.bundler bundler.toml name = "Bari 206"
opt display.notice NOTICE Bari 206
req name.plugin src/lib.rs const NAME: &'static str = "Bari 206"
req vendor.plugin src/lib.rs const VENDOR: &'static str = "Novoyuuparosk"
req email.plugin src/lib.rs const EMAIL: &'static str = "mikkeli@novoyuuparosk.org"
req authors.cargo Cargo.toml authors = ["Mikkeli Matlock <mikkeli@novoyuuparosk.org>"]
req homepage.cargo Cargo.toml homepage = "https://github.com/mikkeli/codename-206"
req url.plugin src/lib.rs const URL: &'static str = env!("CARGO_PKG_HOMEPAGE")
req description.cargo Cargo.toml description = "A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
req description.clap src/lib.rs Some("Multiband compressor/limiter (stage 3: 3-band + 'All' channel)")
req clapid.plugin src/lib.rs const CLAP_ID: &'static str = "org.novoyuuparosk.bari-206"
req vst3id.plugin src/lib.rs const VST3_CLASS_ID: [u8; 16] = *b"Bari206Multiband"
req struct.def src/lib.rs struct Bari206 {
req struct.plugin src/lib.rs impl Plugin for Bari206 {
req struct.clap src/lib.rs impl ClapPlugin for Bari206 {
req struct.vst3 src/lib.rs impl Vst3Plugin for Bari206 {
req struct.exportclap src/lib.rs nih_export_clap!(Bari206);
req struct.exportvst3 src/lib.rs nih_export_vst3!(Bari206);
req params.def src/params.rs struct Bari206Params {
req params.default src/params.rs impl Default for Bari206Params {
req params.field src/lib.rs params: Arc<Bari206Params>,
opt deploy.pkg deploy.ps1 $Package = "bari_206"
opt deploy.vst3 deploy.ps1 $vst3Name = "Bari 206.vst3"
opt deploy.clap deploy.ps1 $clapName = "Bari 206.clap"
Can't render this file because it contains an unexpected character in line 1 and column 33.
+66
View File
@@ -0,0 +1,66 @@
# 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 ------------------------------------------------------------
_derive_pkg() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -e 's/[^a-z0-9]\{1,\}/_/g' -e 's/^_//' -e 's/_$//'
}
_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="Bari 206"
VENDOR="Novoyuuparosk"
EMAIL="mikkeli@novoyuuparosk.org"
AUTHOR="Mikkeli Matlock"
HOMEPAGE="https://github.com/mikkeli/codename-206"
DESCRIPTION="A VST3/CLAP multiband compressor/limiter with a custom gain-curve display"
# What the plugin does *now*. No codename, no development stage.
CLAP_DESCRIPTION="Multiband compressor/limiter (stage 3: 3-band + 'All' channel)"
# --- frozen identity ---------------------------------------------------------
# Hosts key saved projects and presets off these two. Never move them after the
# first release: every existing session silently loses the plugin.
CLAP_ID="org.novoyuuparosk.bari-206"
VST3_CLASS_ID="Bari206Multiband"
# --- fixed at repo creation --------------------------------------------------
# The repo keeps its development codename while the plugin carries the product
# name. Recorded only so it can be kept out of user-facing text.
CODENAME="codename-206"
LIB_FILE="src/lib.rs"
PARAMS_FILE="src/params.rs"
# --- derived unless overridden -----------------------------------------------
# PKG, STRUCT and PARAMS_STRUCT all derive correctly from PLUGIN_NAME here, so
# none is pinned. DISPLAY 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 -------------------------------------------------------
STALE="codename_206
Codename 206
Codename206
Codename206Params
Codename206Maxi!
206 prototype
com.mikkeli.codename-206
bari_206
Bari 206
Bari206
Bari206Params
Bari206Multiband
org.novoyuuparosk.bari-206"
+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 -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<()> { fn main() -> nih_plug_xtask::Result<()> {
nih_plug_xtask::main() nih_plug_xtask::main()
} }