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
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,22 @@ The default Debug build produces the same artifacts at roughly 1 to 2 MB each.

## Current state

The core library, the tests, the build pipeline, and the non-plugin examples all
work. The VST3 factory in `examples/danzig-gain` is a stub: `getClassInfo` and
`createInstance` do not yet produce a class or an object, so a host scans the
bundle and reports zero plugins. See
[Current state](docs/WIKI.md#current-state) for the detail and for what
finishing it involves.
The core library, the tests, the build pipeline, the examples, and the VST3
plugin all work. `examples/danzig-gain` builds a universal `.vst3` bundle that a
host loads and instantiates, and it passes pluginval at strictness level 5:

```
pluginval --validate zig-out/DanzigGain.vst3 --strictness-level 5
Num plugins found: 1
Testing plugin: VST3-Danzig Gain
...
SUCCESS
```

The factory returns a populated `PClassInfo`, and `createInstance` builds one
object exposing `IComponent`, `IAudioProcessor`, and `IEditController` over a
shared lock-free parameter store. See
[Current state](docs/WIKI.md#current-state) for the detail.

## Documentation

Expand Down
99 changes: 56 additions & 43 deletions docs/WIKI.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ anywhere Zig runs, though only macOS is tested.

## Current state

Honest summary, because the difference matters if you are choosing a framework.
danzig builds a VST3 plugin that a host loads, instantiates, and validates.

**Working and tested.**

Expand All @@ -86,42 +86,32 @@ Honest summary, because the difference matters if you are choosing a framework.
serving the web UI, and a native window with an embedded WebView and
CoreAudio device enumeration.

**Not finished.**
**The plugin validates.**

The factory in `examples/danzig-gain` is a stub. `countClasses` returns 1, but
`getClassInfo` writes nothing into the host's buffer and `createInstance`
returns without producing an object. A host therefore scans the bundle, finds
the entry point, and reports zero usable classes:
The factory in `examples/danzig-gain` is complete. `getClassInfo` returns a
populated `PClassInfo`, and `createInstance` builds one object exposing
`IComponent`, `IAudioProcessor`, and `IEditController` over a shared lock-free
parameter store. A host scans the bundle, finds the class, and instantiates it.
pluginval passes at strictness level 5:

```bash
/Applications/pluginval.app/Contents/MacOS/pluginval \
--validate zig-out/DanzigGain.vst3 --strictness-level 5 --timeout-ms 20000
--validate zig-out/DanzigGain.vst3 --strictness-level 5 --timeout-ms 30000
```

```
Started validating: .../danzig/zig-out/DanzigGain.vst3
Random seed: 0x6afa9d8
Validation started
Strictness level: 5
-----------------------------------------------------------------
Starting tests in: pluginval / Scan for plugins located in: .../DanzigGain.vst3...
Num plugins found: 0
!!! Test 1 failed: No types found. This usually means the plugin binary is missing
or damaged, an incompatible format or that it is an AU that isn't found by macOS
so can't be created.
FAILED!! 1 test failed, out of a total of 1
FAILURE
*** FAILED
Num plugins found: 1
Testing plugin: VST3-Danzig Gain-...
Superelectric: Danzig Gain v0.1.0
...
SUCCESS
```

Completing it means filling in `getClassInfo` with a populated `PClassInfo`
(class ID, cardinality, category string, name) and having `createInstance`
return objects implementing `IComponent`, `IAudioProcessor`, and
`IEditController`. The interface declarations for all three already exist in
`src/vst3.zig`. The wiring does not.

So: use danzig today as a DSP and parameter library with a working VST3 build
pipeline. The last mile into a DAW is the open work.
Nineteen test groups run, from instantiation and bus layouts through parameter
automation and state round-trips, with no failures. So danzig loads in a DAW as
a working gain plugin, and doubles as a DSP and parameter library with a
validated VST3 build pipeline.

---

Expand Down Expand Up @@ -340,11 +330,11 @@ Architectures in the fat file: /Users/you/Library/Audio/Plug-Ins/VST3/DanzigGain

### 6. Load it in a DAW

Restart your DAW so it rescans the plugin folder. As of today the scan finds the
bundle and the entry point but reports no instantiable classes, for the reason
described under [Current state](#current-state). The bundle structure, the
universal binary, the `Info.plist`, and the ad-hoc signature are all correct and
verifiable:
Restart your DAW so it rescans the plugin folder. The scan finds the bundle,
instantiates the class, and lists "Danzig Gain" under Superelectric. It also
passes pluginval at strictness level 5 (see [Current state](#current-state)).
The bundle structure, the universal binary, the `Info.plist`, and the ad-hoc
signature are verifiable:

```bash
codesign -dvv zig-out/DanzigGain.vst3
Expand Down Expand Up @@ -700,13 +690,22 @@ the built `DanzigGain` plugin and calls into it the way a host does.
```
danzig integration harness

VST3 module entry
ok bundleEntry accepts the load and reports success

VST3 factory ABI
ok GetPluginFactory returns a non-null object
ok countClasses reports one exported class
ok addRef/release move the count by exactly one
ok queryInterface for an unknown IID reports failure
ok getFactoryInfo returns kResultOk
ok getClassInfo(0) returns kResultOk
...
ok createInstance accepts the advertised class id
ok the object hands back IAudioProcessor
ok the object hands back IEditController
...
ok setupProcessing accepts 48 kHz
ok process returns kResultOk
ok the default 0 dB setting passes DC through at unity
ok bypass passes the input through untouched
ok releasing the last reference drops the count to zero

danzig static library
ok AudioBuffer reports its geometry
Expand All @@ -717,7 +716,10 @@ danzig static library
all integration checks passed
```

The harness declares its own copy of the `IPluginFactory` vtable rather than
About fifty checks run in all, covering the factory, `createInstance`, the
interface queries, the bus and parameter reports, and a full
`setupProcessing`/`setActive`/`process` pass whose output is checked against the
DSP. The harness declares its own copy of the `IPluginFactory` vtable rather than
importing the plugin's Zig types. It reads the first word of the returned
pointer as a vtable pointer and calls through the C function pointers. If the
object layout ever stops matching what a host expects, the dereference fails
Expand All @@ -728,9 +730,9 @@ It returns a non-zero exit code on failure, so `zig build test` fails with it.
### CI

`.github/workflows/ci.yml` runs `zig build` and `zig build test` on `macos-15`
against both 0.14.1 and 0.15.2. The runner is pinned to `macos-15` rather than
`macos-latest`, because `macos-latest` now ships an Xcode whose SDK Zig 0.14.1
cannot link against.
against 0.14.1, 0.15.2, and 0.16.0. The runner is pinned to `macos-15` rather
than `macos-latest`, because `macos-latest` now ships an Xcode whose SDK Zig
0.14.1 cannot link against.

---

Expand Down Expand Up @@ -778,9 +780,9 @@ If the bundle itself is single-architecture, `zig build vst3` did not run.

### The DAW does not list the plugin

Expected today. See [Current state](#current-state). The factory's
`getClassInfo` and `createInstance` are stubs, so a host finds zero classes.
Confirm the bundle is otherwise sound:
The plugin validates (see [Current state](#current-state)), so a missing entry
is usually the host's cache or the install location rather than the bundle.
First confirm the factory symbol is exported:

```bash
nm -gU zig-out/DanzigGain.vst3/Contents/MacOS/DanzigGain | grep -i factory
Expand All @@ -790,6 +792,17 @@ nm -gU zig-out/DanzigGain.vst3/Contents/MacOS/DanzigGain | grep -i factory
00000000000004c8 T _GetPluginFactory
```

Then check the bundle is in `~/Library/Audio/Plug-Ins/VST3/` (see
[Install it](#5-install-it)) and force the host to rescan its plugin list. Many
hosts cache a failed scan, so a bundle fixed after a first bad scan stays hidden
until the cache is cleared. Validate the copy directly with pluginval to rule
the bundle in or out:

```bash
/Applications/pluginval.app/Contents/MacOS/pluginval \
--validate ~/Library/Audio/Plug-Ins/VST3/DanzigGain.vst3 --strictness-level 5
```

### `danzig-webui` starts but the browser shows nothing

The server binds `127.0.0.1:3000`. If something else already holds that port you
Expand Down
56 changes: 40 additions & 16 deletions examples/danzig-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,23 @@ check(factory.vtbl.countClasses(raw.?) == 1, "countClasses reports one exported
If the plugin ever stops putting the vtable pointer in the first machine word,
that dereference fails here rather than inside a DAW.

The checks:

- `GetPluginFactory` returns a non-null object.
- `countClasses` reports one exported class.
- `addRef` and `release` move the reference count by exactly one.
- `queryInterface` for an unknown IID reports failure instead of handing back a
garbage pointer.
- `getFactoryInfo` and `getClassInfo(0)` return `kResultOk`.
The checks run the whole plugin lifecycle through the C ABI:

- The module entry point (`bundleEntry`) accepts the load.
- The factory: `GetPluginFactory`, `countClasses`, `getFactoryInfo`,
`getClassInfo`, and `queryInterface` refusing an unknown IID without handing
back a garbage pointer.
- `createInstance` refuses an unknown class id and builds a real object for the
advertised one, and that object counts references and hands back
`IAudioProcessor` and `IEditController` as distinct interface pointers.
- The component reports one input and one output bus with two channels and
named buses; the controller exposes the gain and bypass parameters and round
trips a parameter display string.
- The processing path: `setupProcessing` at 48 kHz, `setActive`,
`setProcessing`, and a `process` call whose output is checked against the DSP,
0 dB passing DC at unity, +6 dB scaling by ~1.995, and bypass passing the
input through untouched.
- `terminate`, then releasing the last reference drops the count to zero.
- The linked static library still converts dB correctly and the `ParamStore`
reaches full scale.

Expand All @@ -50,13 +59,24 @@ zig build test-integration
```
danzig integration harness

VST3 module entry
ok bundleEntry accepts the load and reports success

VST3 factory ABI
ok GetPluginFactory returns a non-null object
ok countClasses reports one exported class
ok addRef/release move the count by exactly one
ok queryInterface for an unknown IID reports failure
ok getFactoryInfo returns kResultOk
ok getClassInfo(0) returns kResultOk
...
ok createInstance accepts the advertised class id
ok createInstance writes a non-null object
ok the object hands back IAudioProcessor
ok the object hands back IEditController
...
ok setupProcessing accepts 48 kHz
ok process returns kResultOk
ok the default 0 dB setting passes DC through at unity
ok a +6 dB gain setting scales DC by ~1.995
ok bypass passes the input through untouched
ok releasing the last reference drops the count to zero

danzig static library
ok AudioBuffer reports its geometry
Expand All @@ -67,6 +87,8 @@ danzig static library
all integration checks passed
```

The run has about fifty checks; the full list prints when you run it.

It exits non-zero if any check fails.

## Run it with the unit tests
Expand Down Expand Up @@ -97,10 +119,12 @@ exit=0

## What it does not check

It does not create a plugin instance, because `createInstance` in
`examples/danzig-gain` does not yet produce one. When that is implemented, the
natural next checks are `setupProcessing`, `setActive`, and a `process` call
against a known input buffer.
It exercises 32-bit processing on mono and stereo, which is what the plugin
supports. It does not drive 64-bit processing or an editor view, both of which
the plugin declines on purpose (`canProcessSampleSize` refuses 64-bit and
`createView` returns null for the host's generic editor). The full host-side
validation, including automation and state round-trips under a real host, is
what `pluginval` covers; see [docs/WIKI.md](../../docs/WIKI.md#current-state).

---

Expand Down
Loading