Table of Contents
- Overview
- Installation & Setup
- Quick Start
- Core Concepts
- API Reference
- Plugin Development Guide
- Audio Processing
- Parameter System
- Build & Deployment
- Advanced Topics
- Examples
- Troubleshooting
Danzig is a zero-dependency VST3 plugin development framework written entirely in Zig. It provides type-safe abstractions for creating audio plugins without external dependencies, using only the Zig standard library.
- Minimal: Only essential abstractions, no bloat
- Modern: Leverages Zig's memory safety and type system
- Fast: Direct VST3 C ABI, zero overhead
- Educational: Clear, understandable code for learning VST3
Built on the Azazel build system with CUE-based configuration for reproducible, modular builds.
-
Zig 0.14.0 or later
# macOS (using Homebrew) brew install zig # Or download from https://ziglang.org
-
CUE (optional, for build configuration)
brew install cue
-
Git (to clone the repository)
cd /Users/abhishekshivakumar/vex_zig/azazel
git status # Verify you're in the repoThe danzig framework is already integrated into this azazel project!
zig build
./zig-out/bin/danzig_testExpected output:
✓ Test executable compiles and links with danzig library
✓ Allocator initialized
✓ Danzig library linking successful!
cd /Users/abhishekshivakumar/vex_zig/azazel
# Build all targets
zig build
# Or build specific module
zig build danzig_gain
# Clean build
rm -rf zig-cache && zig buildmkdir -p examples/my-first-pluginCreate examples/my-first-plugin/root.zig:
const std = @import("std");
const danzig = @import("danzig");
pub const MyPlugin = struct {
plugin: danzig.Plugin,
pub fn init(allocator: std.mem.Allocator) !*MyPlugin {
const self = try allocator.create(MyPlugin);
self.plugin = danzig.Plugin.init(allocator);
// Add a simple parameter
var param = danzig.Parameter{
.id = 0,
.normalized = 0.5,
.plain = 0.0,
.minValue = -100.0,
.maxValue = 100.0,
.defaultValue = 0.0,
};
@memcpy(param.title[0..9], "My Param\x00");
try self.plugin.addParameter(param);
return self;
}
pub fn process(self: *MyPlugin, inputs: []*[*]f32, outputs: []*[*]f32, channels: u32, samples: u32) void {
// Simple pass-through for now
for (0..channels) |ch| {
@memcpy(outputs[ch][0..samples], inputs[ch][0..samples]);
}
}
};
pub fn main() !void {
std.debug.print("My Plugin - ready to build!\n", .{});
}Edit project.cue:
my_first_plugin: #Module & {
kind: "shared"
root: "examples/my-first-plugin/root.zig"
deps: ["danzig"]
}Edit export.cue:
_modules: {
"my_first_plugin": my_first_plugin
}bash gen_build_spec.sh
zig buildYour plugin is at: zig-out/lib/libmy_first_plugin.dylib
Every Danzig plugin consists of:
┌─────────────────────────────────────┐
│ VST3 Host (DAW) │
└────────────┬────────────────────────┘
│ VST3 API (C ABI)
┌────────────▼────────────────────────┐
│ VST3 Factory Interface │
│ ├─ createInstance() │
│ └─ getFactory() │
└────────────┬────────────────────────┘
│
┌────────────▼────────────────────────┐
│ IComponent (Processor) │
│ ├─ getBusCount() │
│ ├─ getParameterCount() │
│ ├─ setState() │
│ └─ getState() │
└────────────┬────────────────────────┘
│
┌────────────▼────────────────────────┐
│ IAudioProcessor (This is you!) │
│ ├─ process() ← Audio in/out │
│ ├─ activate() ← Start DSP │
│ └─ deactivate() ← Stop DSP │
└─────────────────────────────────────┘
// 1. Instantiation
var plugin = danzig.Plugin.init(allocator);
// 2. Configuration
plugin.setupProcessing(sampleRate, blockSize);
try plugin.addParameter(param);
// 3. Activation
plugin.activate();
// 4. Processing (called repeatedly by host)
plugin.process(inputs, outputs, channels, numSamples);
// 5. Deactivation
plugin.deactivate();
// 6. Cleanup
plugin.deinit();Danzig uses Zig's allocator pattern for memory management:
// Create a general-purpose allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Pass to Danzig
var plugin = danzig.Plugin.init(allocator);This gives you:
- Explicit memory management - no hidden allocations
- Safety - detects leaks and use-after-free
- Control - know exactly where memory is allocated
Parameters are the "knobs" on your plugin:
// Definition
pub const Parameter = struct {
id: u32, // Unique identifier
normalized: f64, // 0.0 to 1.0 (from host)
plain: f64, // Actual value (your code)
title: [128]u8, // Display name
units: [64]u8, // Unit label ("dB", "Hz", etc)
minValue: f64, // Minimum plain value
maxValue: f64, // Maximum plain value
defaultValue: f64, // Initial value
stepCount: i32, // 0 = continuous, >0 = stepped
};Normalization converts between 0.0-1.0 range and actual values:
// Host sends normalized values (0.0-1.0)
plugin.setParameterNormalized(paramId, 0.75);
// You work with plain values
let gain_db = danzig.denormalize(0.75, -48.0, 48.0); // Result: 12.0 dBInitialize a plugin instance.
var plugin = danzig.Plugin.init(allocator);Clean up plugin resources.
defer plugin.deinit();Add a parameter to the plugin.
try plugin.addParameter(.{
.id = 0,
.minValue = -48.0,
.maxValue = 48.0,
.defaultValue = 0.0,
});Get total number of parameters.
const count = plugin.getParameterCount();Set parameter from normalized value (0.0-1.0).
plugin.setParameterNormalized(0, 0.5); // Set to middleGet normalized parameter value.
const normalized = plugin.getParameterNormalized(0);Configure for audio processing.
plugin.setupProcessing(44100.0, 256); // 44.1 kHz, 256 sample blocksStart processing audio. Called when plugin is inserted or DAW starts playback.
plugin.activate(); // Now ready for process() callsStop processing. Clean up any DSP state.
plugin.deactivate();Plugin.process(self: *Plugin, inputs: []*[*]f32, outputs: []*[*]f32, channels: u32, samples: u32) void
Process audio block. This is called repeatedly by the host.
plugin.process(inputs, outputs, 2, 256); // 2 channels, 256 samplesConvert plain value to normalized (0.0-1.0).
const normalized = danzig.normalize(50.0, 0.0, 100.0); // Result: 0.5Convert normalized value to plain.
const plain = danzig.denormalize(0.75, -48.0, 48.0); // Result: 12.0Apply smooth gain changes.
pub const GainProcessor = struct {
gain: f32, // Current gain (linear)
targetGain: f32, // Target gain (linear)
pub fn setGain(self: *GainProcessor, gainDb: f32) void
pub fn process(self: *GainProcessor, inputs: []*[*]f32, outputs: []*[*]f32, channels: u32, samples: u32) void
pub fn getNormalizedGain(self: GainProcessor) f32
pub fn setNormalizedGain(self: *GainProcessor, normalized: f32) void
};Example:
var gainProc = danzig.GainProcessor{};
gainProc.setGain(-6.0); // -6 dB
gainProc.process(inputs, outputs, 2, 256);Smooth parameter ramping over samples.
var ramp = danzig.SimpleRamp.init(0.0, 44100); // Initial value, ramp samples
ramp.setTarget(1.0); // Ramp to 1.0 over 44100 samples
for (0..44100) |_| {
let value = ramp.next();
}Multi-channel audio buffer with memory management.
var buf = try danzig.AudioBuffer.init(allocator, 2, 256, 44100.0);
defer buf.deinit(allocator);
buf.silence(); // Clear to zero
buf.clear(); // Same as silenceLinear interpolation between two values.
const value = danzig.linearInterpolate(0.0, 1.0, 0.5); // Result: 0.5Constrain value to range.
const clamped = danzig.clamp(1.5, 0.0, 1.0); // Result: 1.0Convert dB to linear gain.
const linear = danzig.dBToLinear(-6.0); // Result: ~0.501Convert linear gain to dB.
const db = danzig.linearTodB(0.5); // Result: ~-6.02 dBHere's a complete 3-band parametric EQ example:
const std = @import("std");
const danzig = @import("danzig");
const ParamID = struct {
pub const LowGain: u32 = 0;
pub const MidGain: u32 = 1;
pub const HighGain: u32 = 2;
};
pub const EQPlugin = struct {
plugin: danzig.Plugin,
lowGain: f32 = 1.0,
midGain: f32 = 1.0,
highGain: f32 = 1.0,
pub fn init(allocator: std.mem.Allocator) !*EQPlugin {
const self = try allocator.create(EQPlugin);
self.plugin = danzig.Plugin.init(allocator);
// Low band
var lowParam = danzig.Parameter{
.id = ParamID.LowGain,
.normalized = 0.5,
.minValue = -12.0,
.maxValue = 12.0,
.defaultValue = 0.0,
};
@memcpy(lowParam.title[0..7], "Low EQ\x00");
@memcpy(lowParam.units[0..2], "dB");
try self.plugin.addParameter(lowParam);
// Mid band
var midParam = danzig.Parameter{
.id = ParamID.MidGain,
.normalized = 0.5,
.minValue = -12.0,
.maxValue = 12.0,
.defaultValue = 0.0,
};
@memcpy(midParam.title[0..7], "Mid EQ\x00");
try self.plugin.addParameter(midParam);
// High band
var highParam = danzig.Parameter{
.id = ParamID.HighGain,
.normalized = 0.5,
.minValue = -12.0,
.maxValue = 12.0,
.defaultValue = 0.0,
};
@memcpy(highParam.title[0..8], "High EQ\x00");
try self.plugin.addParameter(highParam);
return self;
}
pub fn deinit(self: *EQPlugin, allocator: std.mem.Allocator) void {
self.plugin.deinit();
allocator.destroy(self);
}
pub fn setParameterNormalized(self: *EQPlugin, paramId: u32, normalized: f64) void {
self.plugin.setParameterNormalized(paramId, normalized);
const db = danzig.denormalize(normalized, -12.0, 12.0);
switch (paramId) {
ParamID.LowGain => self.lowGain = danzig.dBToLinear(@floatCast(db)),
ParamID.MidGain => self.midGain = danzig.dBToLinear(@floatCast(db)),
ParamID.HighGain => self.highGain = danzig.dBToLinear(@floatCast(db)),
else => {},
}
}
pub fn process(self: *EQPlugin, inputs: []*[*]f32, outputs: []*[*]f32, channels: u32, samples: u32) void {
// Simple gain-based "EQ" (real implementation would use filters)
for (0..channels) |ch| {
for (0..samples) |s| {
var sample = inputs[ch][s];
// Apply gains (this is simplified - real EQ uses filters)
sample *= (self.lowGain + self.midGain + self.highGain) / 3.0;
outputs[ch][s] = danzig.clamp(sample, -1.0, 1.0);
}
}
}
};
pub fn main() !void {
std.debug.print("3-Band EQ Plugin\n", .{});
}- Define parameters (knobs/controls)
- Sketch audio algorithm
- Plan DSP implementation
pub const MyPlugin = struct {
plugin: danzig.Plugin,
// Your state variables
pub fn init(allocator) !*MyPlugin { ... }
pub fn process(...) void { ... }
};try self.plugin.addParameter(.{
.id = 0,
.minValue = min,
.maxValue = max,
.defaultValue = default,
});pub fn process(self: *MyPlugin, inputs, outputs, channels, samples) {
for (0..channels) |ch| {
for (0..samples) |s| {
outputs[ch][s] = self.dsp(inputs[ch][s]);
}
}
}- Add to build config
- Build:
zig build - Test in DAW
- Debug with debug symbols
Host Buffer (interleaved or deinterleaved)
↓
Your plugin.process()
├─ Read from inputs[]
├─ Apply DSP
└─ Write to outputs[]
↓
Host Buffer
Danzig uses deinterleaved floating-point audio:
// inputs and outputs are arrays of pointers
// inputs[0] → left channel samples
// inputs[1] → right channel samples
// inputs[0][0..numSamples] → all left channel samples
for (0..numChannels) |ch| {
for (0..numSamples) |s| {
let sample = inputs[ch][s]; // Read
outputs[ch][s] = process(sample); // Write
}
}for (0..numChannels) |ch| {
for (0..numSamples) |s| {
outputs[ch][s] = inputs[ch][s] * gainAmount;
}
}const in = inputs[ch][s];
const out = std.math.tanh(in * driveAmount);
outputs[ch][s] = out;pub const DelayPlugin = struct {
plugin: danzig.Plugin,
delayBuffer: [*]f32,
delayTime: f32,
writePos: u32 = 0,
pub fn process(self: *DelayPlugin, inputs, outputs, channels, samples) {
for (0..channels) |ch| {
for (0..samples) |s| {
// Read from delay
let readPos = (self.writePos -| @intFromFloat(self.delayTime)) % BUFFER_SIZE;
let delayed = self.delayBuffer[readPos];
// Write to output (mix)
outputs[ch][s] = (inputs[ch][s] + delayed) * 0.5;
// Write to delay buffer
self.delayBuffer[self.writePos] = inputs[ch][s];
self.writePos = (self.writePos + 1) % BUFFER_SIZE;
}
}
}
};var gainParam = danzig.Parameter{
.id = 0,
.minValue = -48.0,
.maxValue = 12.0,
.stepCount = 0, // 0 = continuous
};var modeParam = danzig.Parameter{
.id = 1,
.minValue = 0.0,
.maxValue = 3.0,
.stepCount = 3, // Discrete steps
};var bypassParam = danzig.Parameter{
.id = 2,
.minValue = 0.0,
.maxValue = 1.0,
.stepCount = 1, // Only 0 or 1
};pub fn setParameterNormalized(self: *MyPlugin, paramId: u32, normalized: f64) void {
self.plugin.setParameterNormalized(paramId, normalized);
switch (paramId) {
0 => { // Gain
const db = danzig.denormalize(normalized, -48.0, 12.0);
self.gain = danzig.dBToLinear(@floatCast(db));
},
1 => { // Mode
const mode = @as(u32, @intFromFloat(danzig.denormalize(normalized, 0.0, 3.0)));
self.mode = mode;
},
else => {},
}
}my_plugin: #Module & {
kind: "shared" // Produces .dylib/.so/.dll
root: "examples/my-plugin/root.zig"
deps: ["danzig"] // Depends on danzig library
}_modules: {
"my_plugin": my_plugin
}# Regenerate build spec from CUE
bash gen_build_spec.sh
# Build all
zig build
# Build specific module
zig build my_plugin
# Build with optimization
zig build -DReleaseFastzig-out/
├── lib/
│ ├── libdanzig.a # Library
│ └── libmy_plugin.dylib # Your plugin (macOS)
└── bin/
└── executable_targets/
MyPlugin.vst3/
└── Contents/
├── Info.plist
└── MacOS/
└── libmy_plugin.dylib
mkdir -p MyPlugin.vst3/Contents/MacOS
cp zig-out/lib/libmy_plugin.dylib MyPlugin.vst3/Contents/MacOS/
cp path/to/Info.plist MyPlugin.vst3/Contents/// Use Arena allocator for temporary data
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const temp_allocator = arena.allocator();
var temp_data = try temp_allocator.alloc(f32, 1024);// Zig supports manual SIMD
const vec4 = @Vector(4, f32);
var simd_gains = @as(vec4, @splat(self.gain));
var simd_samples: vec4 = .{ in[0], in[1], in[2], in[3] };
var simd_result = simd_samples * simd_gains;pub fn addParameter(self: *Plugin, param: Parameter) !void {
try self.parameters.add(param); // Propagate errors
}
pub fn process(...) !void { // Can return errors
// Allocations
}Danzig plugins run on the audio thread (real-time):
- No allocations during
process() - Pre-allocate all buffers in
setupProcessing() - Use atomic operations for lock-free communication with UI thread
// Good: Allocate once
pub fn setupProcessing(...) {
self.buffer = allocator.alloc(...); // OK
}
// Bad: Don't allocate in process!
pub fn process(...) {
const temp = try allocator.alloc(...); // WRONG!
}pub const GainPlugin = struct {
plugin: danzig.Plugin,
gains: [8]f32 = [_]f32{1.0} ** 8,
pub fn process(self: *GainPlugin, inputs, outputs, channels, samples) {
for (0..channels) |ch| {
for (0..samples) |s| {
outputs[ch][s] = inputs[ch][s] * self.gains[ch];
}
}
}
};pub const CompressorPlugin = struct {
plugin: danzig.Plugin,
threshold: f32,
ratio: f32,
attackTime: f32,
releaseTime: f32,
envelope: f32 = 0.0,
pub fn process(self: *CompressorPlugin, inputs, outputs, channels, samples) {
for (0..samples) |s| {
// Detect peak
var peak: f32 = 0.0;
for (0..channels) |ch| {
peak = @max(peak, @abs(inputs[ch][s]));
}
// Attack/Release envelope
if (peak > self.envelope) {
self.envelope += (peak - self.envelope) * self.attackTime;
} else {
self.envelope -= (self.envelope - peak) * self.releaseTime;
}
// Calculate gain reduction
var gainReduction: f32 = 1.0;
if (self.envelope > self.threshold) {
gainReduction = 1.0 / (1.0 + (self.envelope / self.threshold - 1.0) * (self.ratio - 1.0));
}
// Apply
for (0..channels) |ch| {
outputs[ch][s] = inputs[ch][s] * gainReduction;
}
}
}
};pub const ReverbPlugin = struct {
plugin: danzig.Plugin,
buffers: [4][*]f32,
positions: [4]u32 = [_]u32{0} ** 4,
mix: f32 = 0.5,
pub fn process(self: *ReverbPlugin, inputs, outputs, channels, samples) {
const delay_sizes = [_]u32{ 12345, 23456, 34567, 45678 };
for (0..samples) |s| {
var delayed: f32 = 0.0;
for (0..4) |buf| {
delayed += self.buffers[buf][self.positions[buf]];
self.buffers[buf][self.positions[buf]] = inputs[0][s];
self.positions[buf] = (self.positions[buf] + 1) % delay_sizes[buf];
}
for (0..channels) |ch| {
outputs[ch][s] = danzig.linearInterpolate(inputs[ch][s], delayed * 0.25, self.mix);
}
}
}
};Error: "no module named 'danzig'"
- Make sure
deps: ["danzig"]is inproject.cue - Verify
"danzig": danzigis inexport.cue - Run
bash gen_build_spec.sh
Compilation errors
- Check Zig version:
zig version(need 0.14.0+) - Look for unused parameters/variables (Zig is strict)
- Use
_to ignore unused values:pub fn foo(_: u32) {}
Linking errors
- Verify plugin depends on danzig:
deps: ["danzig"] - Check that danzig module is built first
- Ensure output paths are correct
Plugin crashes on activate
- Don't allocate in
activate(), do it insetupProcessing() - Check allocator isn't null
- Verify buffer sizes
Audio distorts
- Check clipping: use
danzig.clamp(value, -1.0, 1.0) - Monitor gain levels
- Avoid NaN/Inf values
Parameter changes don't work
- Verify parameter ID is correct
- Check normalization range (should be 0.0-1.0)
- Ensure
setParameterNormalizedis called from host
Plugin not recognized by DAW
- Check VST3 bundle structure
- Verify
_GetPluginFactoryexports:nm zig-out/lib/libmy_plugin.dylib | grep GetPluginFactory - Try loading in VST validator tool
Clicks/pops in audio
- Reduce allocations (pre-allocate buffers)
- Use fast math (avoid expensive ops in audio loop)
- Profile with profiler
High CPU usage
- Optimize DSP (use SIMD, avoid expensive functions)
- Reduce parameter smoothing resolution
- Cache computed values
✓ Use allocators, not raw malloc
✓ Pre-allocate in setupProcessing(), not process()
✓ Use defer for cleanup
✗ Don't leak memory in errors
✓ Clamp outputs to [-1.0, 1.0] ✓ Handle NaN/Inf gracefully ✓ Use ramps for parameter changes (no clicks) ✗ Don't trust input ranges
✓ Use meaningful IDs (0, 1, 2...) ✓ Provide default values ✓ Document value ranges ✗ Don't change parameter count after init
✓ Keep audio processing simple/fast
✓ Use fixed-size allocations
✓ Profile real code
✗ Don't do expensive work in process()
- VST3 Specification: https://steinbergmedia.github.io/vst3_dev_portal/
- Zig Documentation: https://ziglang.org/documentation/
- Audio DSP: https://www.dsprelated.com/
- Danzig Source:
src/danzig/in this repository
For issues or questions:
- Check this documentation
- Review examples in
examples/danzig-gain/ - Look at source code in
src/danzig/ - Check build logs with
zig build(verbose output)
Last Updated: 2026-03-15 Danzig Version: 1.0 Zig Requirement: 0.14.0+