Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,202 changes: 1,202 additions & 0 deletions scripts/build_site.py

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions site/architecture.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Architecture · danzig</title>
<meta name="description" content="How danzig expresses the VST3 COM ABI as Zig extern structs, how a plugin registers itself, and what the audio callback path looks like.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">danzig</a>
<div class="tagline">VST3 plugin framework in pure Zig</div>
<span class="version">v0.1.0</span>
<div class="section-label">Guide</div>
<a href="index.html">Overview</a>
<a href="architecture.html" class="active">Architecture</a>
<a href="getting-started.html">Getting Started</a>
<div class="section-label">Reference</div>
<a href="parameters.html">Parameters</a>
<a href="audio-helpers.html">Audio Helpers</a>
<a href="vst3-bundle.html">VST3 Bundle</a>
<div class="section-label">Project</div>
<a href="testing.html">Testing</a>
<a href="examples.html">Examples</a>
<a href="troubleshooting.html">Troubleshooting</a>
<a href="licensing.html">Licensing</a>
<div class="spacer"></div>
<div class="nav-footer">
<a href="https://github.com/godofecht/danzig">GitHub</a>
<a href="https://github.com/godofecht/zaza">Zaza</a>
<a href="https://github.com/godofecht/azazel">Azazel</a>
</div>
</nav>
<main>

<h1 id="architecture">Architecture</h1>

<p class="page-subtitle">COM in Zig, how a plugin is registered, and the audio callback path.</p>

<h3 id="com-in-zig">COM in Zig</h3>

<p>A C++ object with virtual functions is a pointer to a vtable followed by the
object's fields. A COM interface is that, plus the convention that the first
three vtable slots are <code>queryInterface</code>, <code>addRef</code>, and <code>release</code>.</p>

<p><code>src/vst3.zig</code> writes this out as plain Zig:</p>

<pre data-lang="zig"><code class="language-zig">pub const IUnknown = extern struct {
queryInterface: *const fn (?*IUnknown, *const IID, ?*[*]?*anyopaque) callconv(.c) TResult = undefined,
addRef: *const fn (?*IUnknown) callconv(.c) u32 = undefined,
release: *const fn (?*IUnknown) callconv(.c) u32 = undefined,
};</code></pre>

<p>Three things make this work.</p>

<p><code>extern struct</code> guarantees C layout: fields in declaration order, C alignment
rules, no reordering. This is the whole reason the trick is safe.</p>

<p><code>callconv(.c)</code> gives each function pointer the platform C calling convention,
so arguments land in the registers the host expects.</p>

<p>Interface inheritance becomes struct embedding. <code>IComponent</code> starts with an
<code>IPluginBase</code> field, which starts with an <code>IUnknown</code> field. Because <code>extern
struct</code> puts fields at ascending offsets with the first at offset zero, a
<code>*IComponent</code> is bit-identical to a <code>*IPluginBase</code> and to a <code>*IUnknown</code>. That
is exactly what single inheritance produces in C++.</p>

<pre data-lang="zig"><code class="language-zig">pub const IComponent = extern struct {
pluginBase: IPluginBase, // offset 0, itself starting with IUnknown
getControllerClassId: *const fn (?*IComponent, ?*CUID) callconv(.c) TResult = undefined,
setIoMode: ...
};</code></pre>

<p>The rest of <code>vst3.zig</code> is the data the ABI passes around: <code>ProcessData</code>,
<code>AudioBusBuffers</code>, <code>ProcessSetup</code>, <code>ParameterInfo</code>, <code>BusInfo</code>, plus the
<code>TResult</code> constants and bus and media type enums. All <code>extern struct</code>, all
laid out to match the SDK headers.</p>

<h3 id="how-a-plugin-is-registered">How a plugin is registered</h3>

<p>A VST3 binary exports one symbol. That is the entire registration mechanism.</p>

<pre data-lang="zig"><code class="language-zig">export fn GetPluginFactory() ?*anyopaque {
gFactory.vtbl = @ptrCast(&amp;factoryVtable);
return @ptrCast(&amp;gFactory);
}</code></pre>

<p><code>gFactory</code> is a static whose first field is a pointer to a static vtable. The
host receives the address of <code>gFactory</code>, reads the first word to get
<code>&amp;factoryVtable</code>, and calls through it. Nothing is allocated. Nothing is
registered anywhere else. There is no plugin database, no manifest, and no
macro.</p>

<p>From there the host does:</p>

<ol>
<li><code>countClasses()</code> to learn how many classes the binary exports.</li>
<li><code>getClassInfo(i, &amp;info)</code> for each, reading the class ID, category, and name.</li>
<li><code>createInstance(class_id, iid, &amp;out)</code> to get an object implementing the
requested interface.</li>
</ol>

<p><code>examples/danzig-test</code> performs exactly steps 1 through 3 against the built
plugin, going through the C function pointers rather than through Zig types, so
a layout change that would break a real host breaks the test first.</p>

<h3 id="the-audio-callback-path">The audio callback path</h3>

<p>The host owns the buffers. It hands you a <code>ProcessData</code> describing them and
expects you to be finished by the time the callback returns.</p>

<pre><code>host audio thread
|
+-- IAudioProcessor.setupProcessing(&amp;setup) once, before playback
| sample rate, max block size, 32- or 64-bit samples
|
+-- IAudioProcessor.setProcessing(true) transport starts
|
+-- IAudioProcessor.process(&amp;data) every block, on the audio thread
| data.numSamples
| data.inputs[bus].channelBuffers32[ch]
| data.outputs[bus].channelBuffers32[ch]
|
+-- IAudioProcessor.setProcessing(false) transport stops</code></pre>

<p>Inside <code>process</code> the rules are the usual real-time rules. No allocation, no
locks, no file or network access, no logging that touches a mutex. danzig's
contribution is that the parameter path obeys them by construction: the host's
UI thread writes a normalized <code>f32</code> with an atomic store, and the audio thread
reads it with an atomic load. There is nothing between the two that can block.</p>

<p>A minimal per-sample loop looks like this, from
<code>examples/danzig-minimal/root.zig</code>:</p>

<pre data-lang="zig"><code class="language-zig">pub fn process(
self: *MinimalPlugin,
input: []const []const f32,
output: []const []f32,
frames: usize,
) void {
for (0..frames) |i| {
const gain = danzig.dBToLinear(self.params.tick(ParamIndex.trim));
for (input, output) |in_ch, out_ch| {
out_ch[i] = in_ch[i] * gain;
}
}
}</code></pre>

<p><code>tick</code> advances the smoother by one sample and returns the plain value, so a
parameter change becomes a ramp rather than a step. That is what stops a slider
drag from producing clicks.</p>

<div class="page-footer">
<a href="index.html">&larr; Overview</a>
<a href="getting-started.html">Next: Getting Started &rarr;</a>
</div>

</main>
</body>
</html>
110 changes: 110 additions & 0 deletions site/audio-helpers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Audio Helpers · danzig</title>
<meta name="description" content="dBToLinear, linearTodB, GainProcessor, SimpleRamp, and AudioBuffer, the small dependency-free DSP helpers in src/audio.zig.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">danzig</a>
<div class="tagline">VST3 plugin framework in pure Zig</div>
<span class="version">v0.1.0</span>
<div class="section-label">Guide</div>
<a href="index.html">Overview</a>
<a href="architecture.html">Architecture</a>
<a href="getting-started.html">Getting Started</a>
<div class="section-label">Reference</div>
<a href="parameters.html">Parameters</a>
<a href="audio-helpers.html" class="active">Audio Helpers</a>
<a href="vst3-bundle.html">VST3 Bundle</a>
<div class="section-label">Project</div>
<a href="testing.html">Testing</a>
<a href="examples.html">Examples</a>
<a href="troubleshooting.html">Troubleshooting</a>
<a href="licensing.html">Licensing</a>
<div class="spacer"></div>
<div class="nav-footer">
<a href="https://github.com/godofecht/danzig">GitHub</a>
<a href="https://github.com/godofecht/zaza">Zaza</a>
<a href="https://github.com/godofecht/azazel">Azazel</a>
</div>
</nav>
<main>

<h1 id="the-audio-helpers">The Audio Helpers</h1>

<p><code>src/audio.zig</code>. Small, dependency-free, and covered by the unit tests.</p>

<h3 id="dbtolinear-and-lineartodb">dBToLinear and linearTodB</h3>

<pre data-lang="zig"><code class="language-zig">pub fn dBToLinear(dB: f32) f32 {
return @exp(dB * 0.11512925464970229); // ln(10)/20
}

pub fn linearTodB(linear: f32) f32 {
if (linear &lt;= 0.0) return -80.0;
return @log(linear) * 8.6858896380650365; // 20/ln(10)
}</code></pre>

<p>Both avoid <code>pow</code> and <code>log10</code> in favour of a single <code>exp</code> or <code>log</code> and a
multiply. <code>linearTodB</code> floors at -80 dB for non-positive input, so silence
returns a finite number rather than negative infinity.</p>

<p>The constants are worth a test of their own, and they have one. A previous copy
of this file carried a stray factor of ten in the exponent, which turned
<code>dBToLinear(6)</code> into 1000.0 instead of 1.9953. <code>src/tests.zig</code> now checks unity
at 0 dB, the factor of two at +6 dB, the factor of ten at +20 dB, and a full
round trip across -48 to +24 dB.</p>

<h3 id="gainprocessor">GainProcessor</h3>

<p>A gain stage with a built-in ramp.</p>

<pre data-lang="zig"><code class="language-zig">var g = danzig.GainProcessor{};
g.setGain(6.0); // dB
g.process(&amp;inputs, &amp;outputs, channels, frames);</code></pre>

<p><code>setGain</code> converts to a linear target. <code>process</code> interpolates the current gain
toward the target by a fixed 0.001 per sample, so a change takes roughly a
thousand samples to substantially complete. <code>setNormalizedGain</code> maps <code>[0, 1]</code>
onto -48 to +48 dB, which is the range the example plugin exposes.</p>

<p>The interpolation coefficient is fixed and not sample-rate aware. For a
rate-independent ramp, use <code>AtomicParam</code> with a millisecond time constant
instead.</p>

<h3 id="simpleramp">SimpleRamp</h3>

<p>A linear ramp over a sample count, for anything that is not a gain.</p>

<pre data-lang="zig"><code class="language-zig">var r = danzig.SimpleRamp.init(0.0, 8); // start value, ramp length in samples
r.setTarget(1.0);
for (0..8) |_| _ = r.next();
// r.getValue() == 1.0</code></pre>

<p><code>setTarget</code> restarts the ramp from the current value. A ramp length of zero or
one is treated as instant. The final sample is snapped to the target exactly, so
the ramp does not leave a residue.</p>

<h3 id="audiobuffer">AudioBuffer</h3>

<p>An owned multi-channel buffer, for offline work and tests. It allocates, so keep
it off the audio thread.</p>

<pre data-lang="zig"><code class="language-zig">var buf = try danzig.AudioBuffer.init(allocator, 2, 512, 48000.0);
defer buf.deinit(allocator);
buf.clear();</code></pre>

<p><code>init</code> zeroes every channel. <code>clear</code> and its alias <code>silence</code> re-zero.</p>

<div class="page-footer">
<a href="parameters.html">&larr; Parameters</a>
<a href="vst3-bundle.html">Next: VST3 Bundle &rarr;</a>
</div>

</main>
</body>
</html>
62 changes: 62 additions & 0 deletions site/examples.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Examples · danzig</title>
<meta name="description" content="The six example directories in the danzig repository, what each one shows, and the command that runs it.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">danzig</a>
<div class="tagline">VST3 plugin framework in pure Zig</div>
<span class="version">v0.1.0</span>
<div class="section-label">Guide</div>
<a href="index.html">Overview</a>
<a href="architecture.html">Architecture</a>
<a href="getting-started.html">Getting Started</a>
<div class="section-label">Reference</div>
<a href="parameters.html">Parameters</a>
<a href="audio-helpers.html">Audio Helpers</a>
<a href="vst3-bundle.html">VST3 Bundle</a>
<div class="section-label">Project</div>
<a href="testing.html">Testing</a>
<a href="examples.html" class="active">Examples</a>
<a href="troubleshooting.html">Troubleshooting</a>
<a href="licensing.html">Licensing</a>
<div class="spacer"></div>
<div class="nav-footer">
<a href="https://github.com/godofecht/danzig">GitHub</a>
<a href="https://github.com/godofecht/zaza">Zaza</a>
<a href="https://github.com/godofecht/azazel">Azazel</a>
</div>
</nav>
<main>

<h1 id="examples">Examples</h1>

<p>Each directory has its own README with the exact commands.</p>

<div class="table-scroll">
<table>
<thead><tr><th>Example</th><th>What it shows</th><th>Run it</th></tr></thead>
<tbody>
<tr><td><code>examples/danzig-minimal</code></td><td>The smallest complete plugin. Start here.</td><td><code>zig build run-minimal</code></td></tr>
<tr><td><code>examples/danzig-gain</code></td><td>A fuller plugin: <code>Plugin</code>, <code>ParameterMap</code>, <code>GainProcessor</code>, and a factory vtable.</td><td>Built into the <code>.vst3</code> bundle</td></tr>
<tr><td><code>examples/danzig-test</code></td><td>Driving the plugin through the raw VST3 C ABI.</td><td><code>zig build test-integration</code></td></tr>
<tr><td><code>examples/danzig-gain-standalone</code></td><td>Offline WAV processing with the DSP core.</td><td><code>zig build run-standalone</code></td></tr>
<tr><td><code>examples/danzig-webui</code></td><td>A pure-<code>std.net</code> HTTP server serving the web UI.</td><td><code>./zig-out/bin/danzig-webui</code></td></tr>
<tr><td><code>examples/danzig-gain-ui</code></td><td>A native macOS window: WebView UI plus CoreAudio device enumeration.</td><td><code>zig build run-gui</code></td></tr>
</tbody>
</table>
</div>

<div class="page-footer">
<a href="testing.html">&larr; Testing</a>
<a href="troubleshooting.html">Next: Troubleshooting &rarr;</a>
</div>

</main>
</body>
</html>
Loading
Loading