ArborealAudio / arbor

Easy-to-use audio plugin framework
https://arborealaudio.com
MIT License
27 stars 0 forks source link

arbor

For the future of plugin development

Goals

Have:

TODO:

Usage

100 LOC Or Less

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]

Acknowledgements

These open-source libraries and examples were a huge help in getting started:

[^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)