A set of Bazel rules and shared libraries for building cross-platform CLAP/VST3/AUv3 plugins. Uses [clap-wrapper](https://github.com/free-audio/clap-wrapper) for the heavy API lifting. This is not a plugin; it is a Bazel module for plugins to import and use.
  • C++ 76.6%
  • Starlark 19.1%
  • Shell 2.6%
  • Python 1.3%
  • Swift 0.4%
Find a file
bcox af743602aa
All checks were successful
ci / macos (push) Successful in 22s
ci / default (push) Successful in 2m47s
ci / opt (push) Successful in 2m48s
ci / asan (push) Successful in 2m53s
ci / ubsan (push) Successful in 2m55s
ci / tsan (push) Successful in 2m55s
ci / pi (push) Successful in 3m7s
ci / coverage (push) Has been skipped
Merge pull request 'What Procrastinator needs of the kit' (#4) from bcox/procrastinator-hooks into main
Reviewed-on: #4
2026-11-05 00:45:29 +00:00
.forgejo/workflows Merge branch 'refs/heads/main' into mac-ci 2026-11-03 19:55:58 -05:00
examples
platforms moduleinfo.json in the x86-64 Linux packages' .vst3 2026-11-03 21:02:57 -05:00
plugin_kit What Procrastinator needs of the kit 2026-11-04 19:08:25 -05:00
rules An Audio Unit extension's bundle identifier may keep one it had before 2026-11-04 19:12:32 -05:00
third_party
tools Coverage's pages with LCOV 2, as the Mac has 2026-11-03 20:07:14 -05:00
.bazelignore
.bazelrc Merge branch 'refs/heads/main' into mac-ci 2026-11-03 19:55:58 -05:00
.bazelversion
.clangd
.gitignore
BUILD.bazel What Procrastinator needs of the kit 2026-11-04 19:08:25 -05:00
LICENSE
MODULE.bazel Update version to 0.3.0 2026-11-04 19:08:27 -05:00
MODULE.bazel.lock
README.md An Audio Unit extension's bundle identifier may keep one it had before 2026-11-04 19:12:32 -05:00
suite.bzl
THIRD_PARTY_NOTICES.md
version.bzl Update version to 0.3.0 2026-11-04 19:08:27 -05:00

plugin-kit

What every audio plugin of mine is built from, as a Bazel module: a plugin's repository depends on it, and holds only its own sound, window and names. Each plugin it builds is a CLAP and a VST3 on Linux and macOS, and an Audio Unit (AUv3) on macOS. A fix here reaches every plugin when its repository moves its pin forward.

plugin-template is the repository to start from. It's a suite of one plugin, set up as described below.

What is here

Where What
rules/plugin.bzl audio_plugin: one plugin's .clap, .vst3, Audio Unit, and the tests every plugin gets
rules/suite.bzl plugin_suite: every plugin of a repository, as one Mac app and disk image, one install target and one pair of Linux packages, with any files of its own under /usr/share/<package>
plugin_kit/plugin/ plugin.h, the class a plugin subclasses; the CLAP entry and factory; notes.h, which turns CLAP notes, MIDI and MIDI 2.0 into MIDI 1.0 at each event's sample; the window's X11 and Cocoa sides; thread-to-thread helpers
plugin_kit/ui/ the canvas and faces a view draws with, view.h (what a plugin's view is), and the placeholder view
plugin_kit/auv3/ the Audio Unit in front of a plugin, and the app that carries a suite's
plugin_kit/testing/ the tests every plugin gets, and its window-to-PNG tool
examples/starter/ the skeleton plugin: the kit's own test plugin, and what new_plugin copies
tools/new_plugin.sh adds a plugin to a repository
third_party/, MODULE.bazel CLAP, the VST3 SDK, clap-wrapper, stb_truetype and the Goldman font, by hash

A plugin

A plugin is a directory with a cc_library that subclasses plugin_kit::Plugin and defines plugin_kit::MakePlugin, plus one audio_plugin call:

load("@plugin_kit//rules:plugin.bzl", "PLUGIN_COPTS", "audio_plugin")

cc_library(
    name = "plugin",
    srcs = ["plugin.cc"],
    copts = PLUGIN_COPTS,
    deps = ["@plugin_kit//:plugin"],
)

audio_plugin(
    name = "tapeecho",
    display_name = "Tape Echo",
    id = "com.djehuti.tapeecho",
    kind = "effect",            # or "instrument"
    version = VERSION,
    vendor = "djehuti",
    au_manufacturer = "Djht",
    au_subtype = "Tpec",
    icon = "tapeecho.icns",
    deps = [":plugin"],
)
class TapeEcho final : public plugin_kit::Plugin {
 public:
  explicit TapeEcho(const clap_host_t* host) : Plugin(host) {}

 protected:
  void Process(const float* in_left, const float* in_right, float* out_left,
               float* out_right, int frames) override;
  void Midi(const plugin_kit::MidiMessage& message) override;
};

std::unique_ptr<plugin_kit::Plugin> plugin_kit::MakePlugin(const clap_host_t* host) {
  return std::make_unique<TapeEcho>(host);
}

plugin.h lists what else a plugin may override: Activate, Reset, Event for parameters and the transport, Extension for CLAP's state, parameters and the rest, and MakeView for its window. A plugin that takes the host's events itself, to batch them or to send its own, overrides ProcessBlock and is handed the whole of CLAP's process; ProcessingStarted, ProcessingStopped, OnMainThread and WindowHidden are CLAP's calls of those names.

audio_plugin also takes what only some plugins need: features, CLAP's beyond the kind's; note_input = False, for an effect that hears nothing played; entry_hooks = True, when the plugin's code defines plugin_kit/plugin/entry_hooks.h's work at the entry's init and deinit and further factories, as one for presets; and for the Audio Unit, au_state_key, a key its saved state had before, and au_home_directory, a directory of the user's that its sandbox is let at, and au_bundle_suffix, what its extension's bundle identifier had after the app's before. plugin_kit/plugin/audio_unit_ext.h is what an Audio Unit can ask that CLAP cannot: a new parameter list with the sound going on, a tail at a tempo, and user presets. The kit handles audio ports by kind, the note port, splitting the block at each event's sample, the window, the entry, the processor check, the VST3 through clap-wrapper and the Audio Unit.

bazel run @plugin_kit//tools:new_plugin -- NAME SUBTYPE effect|instrument writes such a directory from examples/starter.

A repository that uses it

Its MODULE.bazel has:

bazel_dep(name = "plugin_kit", version = "0.1.0")
git_override(
    module_name = "plugin_kit",
    commit = "...",
    remote = "https://forge.djehuti.com/bcox/plugin-kit.git",
)

It also has the toolchain, which only the root module may declare: toolchains_llvm's clang, a Debian 12 sysroot from @plugin_kit//tools/linux:debian.bzl, and its toolchain constrained to @plugin_kit//platforms:debian12. plugin-template's MODULE.bazel is the one to copy. Its .bazelrc names @plugin_kit//platforms:linux_x86_64 for --config=linux_x86_64, and tools/workspace_status.sh for the commit a window shows.

At its top it has suite.bzl (the suite's identity), version.bzl, plugins.bzl (the list) and a BUILD.bazel calling plugin_suite, all as plugin-template has them.

To work on the kit and a plugin together, build the plugin with --override_module=plugin_kit=../plugin-kit. Then move the pin once the kit's change is committed.

Building the kit itself

bazel test //...                       # its own tests, and examples/starter as a plugin
bazel test --config=asan //...         # also opt, ubsan, tsan
bazel test --test_env=DISPLAY --test_env=XAUTHORITY //...  # and the X11 tests
tools/coverage.sh                      # line coverage, into coverage/
bazel build --config=linux_x86_64 --config=opt --strip=always //examples:packages
bazel build --config=pi //...          # for the Raspberry Pi 3B, aarch64 Linux

--config=pi cross-builds for the Pi as ../mini-800 does: the same clang targeting aarch64 against Debian 12's arm64 glibc, with LLVM's aarch64 libc++ linked statically, its toolchain registered first (MODULE.bazel). It builds only, as nothing here can run aarch64 code, so a .vst3 for it has no moduleinfo.json. A repository that builds for the Pi copies that toolchain and //platforms:pi3_aarch64; the packages say arm64 there.

CI (.forgejo/workflows/ci.yml) runs six lanes on a push to main and on a pull request: the default build with -Werror, opt, asan, ubsan and tsan, each tools/ci_test.sh in a Debian 12 container with Xvfb for the X11 tests, which skip without a display, and pi, which builds everything for the Pi with -Werror and runs nothing. On a pull request a coverage job follows them and posts the coverage of the changed lines on the pull request. Every job asks for a runner labeled x86-64-v3, since the tests run the plugins, which need AVX2 and FMA; a plugin repository's CI needs such a runner too, and for TSan one whose kernel has vm.mmap_rnd_bits at 28 or less (.bazelrc says why). The sanitizer lanes leave out tests tagged no-sanitizer, which is the per-plugin old_cpu_test, run under qemu. Under a sanitizer, --define=plugin_kit_sanitizer=1 links a plugin's shared object without -z defs, since the runtime is the loading program's. A plugin repository with lanes of its own sets that define too (the kit's .bazelrc shows how).

The Audio Unit's code and the Mac's rules build only on macOS, so CI has a macos job too, on the runner labeled macos-arm64: bazel test --copt=-Werror //..., and on a pull request tools/coverage.sh, whose total the coverage comment gives beside the Linux one and whose report is the coverage-macos artifact. The kit is its own root for this, with the toolchain as a dev dependency.

License

Released into the public domain under CC0 1.0 Universal. Third-party parts keep their own terms, listed in THIRD_PARTY_NOTICES.md.