Dead-simple plugin development. Write <= 100 lines of code and have a runnable blank-slate plugin.
Ideally only require Zig as a toolchain dependency, not as a programming language. You should be able to write plugins in C/C++/whatever and easily link that code to Arbor via a C API and the Zig build system.
get_zig.sh
that will download latest stable Zig if you
don't already have itEasy cross-compilation. Compile to Mac/Linux/Windows from Mac/Linux/Windows, batteries included.
Cross-platform graphics. A simple software renderer (like Olivec), but also native graphics programming, potentially using something like sokol, or making a thin wrapper around Direct2D/CoreGraphics for cross-platform graphics abstraction, giving the programmer a simple choice with little-to-no platform-specific considerations.
Simple, declarative UI design. Possibly with the option of using a custom CSS-like syntax (or Ziggy) to declare, arrange, and style UI widgets at runtime or compile-time, all compiling to native code--not running in some god-forsaken web browser embedded in a plugin UI 🤮
A nice abstraction layer over plugin APIs which should lend itself nicely to extending support to other APIs
Easy comptime parameter generation
Basic CLAP audio plugin supporting different types of parameters, sample-accurate automation
A janky VST2 implementation that works in Reaper and mostly works in other DAWs
A basic delay module
"Vicanek" IIR Filters which don't cramp at Nyquist [^1][^2][^3]
Simple, portable software rendering using Olivec and a custom text rendering function with a bitmap font
[ ] Figure out if we can write a binding for VST3 API without getting a lawyer
[ ] AUv2 API
[ ] Improve VST2 format
[ ] Actually do stuff with MIDI (I'm a guitar guy not a synth guy)
[ ] Unit tests
[x] Validating CLAP & VST2 w/ clap-validator & pluginval, respectively
[ ] Write tests for other parts of the library, handling bad data from hosts, etc
[ ] Simple cross-platform rendering
[x] Got basic shapes using Olivec software renderer
[x] Simple bitmap text drawing
[ ] Make text drawing more robust and complete
[ ] Allow importing/creating a custom font bitmap and using that for text rendering
[ ] Text kerning
[ ] Make some basic widgets for building UI:
[x] Slider (vertical slider at least)
[ ] Knob
[ ] Button
[x] Label
[x] Options menu
[ ] Add GUI timer on Linux
[ ] Add a basic volume meter to/as an example
[ ] Simple & robust events system
[x] Decent syncing of parameter changes
[ ] Handle CLAP non-destructive parameter modulation
[ ] Make GUI optional (should allow cross-compiling)
gui_init
This is what starting up a project with Arbor should look like: (NOTE: This is a WIP and won't always reflect how the API actually works. I will try to update to be in sync with changes.)
Run zig init
to create some boilerplate for a Zig project. Or, create a
build.zig
and a build.zig.zon
file at the root of your project, then run:
zig fetch --save https://github.com/ArborealAudio/arbor#[commit SHA]
The commit SHA is the SHA of the commit you wish to checkout. You can supply
master
instead (not recommended) if you want to pull from the repo's head each time, which is less predictable.
In top-level build.zig
:
const std = @import("std");
const arbor = @import("arbor");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
try arbor.addPlugin(b, .{
.description = .{
.id = "com.Plug-O.Evil",
.name = "My Evil Plugin",
.company = "Plug-O Corp, Ltd.",
.version = "0.1.0",
.url = "https://plug-o-corp.biz",
.contact = "contact@plug-o-corp.biz",
.manual = "https://plug-o-corp.biz/Evil/manual.pdf",
.copyright = "(c) 2100 Plug-O-Corp, Ltd.",
.description = "Vintage Analog Warmth",
},
.features = arbor.features.STEREO | arbor.features.EFFECT |
arbor.features.EQ,
.root_source_file = "src/plugin.zig",
.target = target,
.optimize = optimize,
});
}
In plugin.zig
:
const arbor = @import("arbor");
const Mode = enum {
Vintage,
Modern,
Apocalypse,
};
const params = &[_]arbor.Parameter{
arbor.param.Float(
"Gain", // name
0.0, // min
10.0, // max
0.666, // default
.{.flags = .{}}, // can provide additional flags
);
arbor.param.Choice("Mode", Mode.Vintage, .{.flags = .{}});
};
const Plugin = @This();
// specify an allocator if you want
const allocator = std.heap.c_allocator;
// initialize plugin
export fn init() *arbor.Plugin {
const plugin = arbor.init(allocator, params .{
.deinit = deinit,
.prepare = prepare,
.process = process,
});
const user_plugin = allocator.create(Plugin) catch |err| // catch any allocation errors
arbor.log.fatal("Plugin create failed: {!}\n", .{err}, @src());
user_plugin.* = .{}; // init our plugin to default
plugin.user = user_plugin; // set user context pointer
return plugin;
}
fn deinit(plugin: *arbor.Plugin) void {
const plugin: plugin.getUser(Plugin);
plugin.allocator.destroy(plugin);
}
fn prepare(plugin: *arbor.Plugin, sample_rate: f32, max_num_frames: u32) void {
// prepare your effect if needed
_ = plugin;
_ = sample_rate;
_ = max_num_frames;
}
// process audio
fn process(plugin: *arbor.Plugin, buffer: arbor.AudioBuffer(f32)) void {
// load an audio parameter value
const gain_param = plugin.getParamValue(f32, "Gain");
for (buffer.input, 0..) |channel_data, ch_num| {
for (channel_data, 0..) |sample, i| {
buffer.output[ch_num][i] = sample * gain_param;
}
}
}
// TODO: Demo how UI would work
To build:
zig build
# Add 'copy' to copy plugin to user plugin dir
# Eventual compile options:
# You can add -Dformat=[VST2/VST3/CLAP/AU]
# Not providing a format will compile all formats available on your platform
# Cross compile by adding -Dtarget=[aarch64-macos/x86_64-windows/etc...]
# Build modes: -Doptimize=[Debug/ReleaseSmall/ReleaseSafe/ReleaseFast]
These open-source libraries and examples were a huge help in getting started:
The influential and robust CLAP tutorial by Nakst
zig-clap-noise-shaker by GreyDodger was a great starting point to understand how to write Zig while working with a C library
[^1]: "Matched Second Order Filters" by Martin Vicanek (2016)
[^2]: "Matched One-Pole Digital Shelving Filters" by Martin Vicanek (2019)
[^3]: "Matched Two-Pole Digital Shelving Filters" by Martin Vicanek (2024)