Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PlugBrowser

A browsable, searchable catalog of the audio plugins installed on this machine — metadata, and eventually a picture of each plugin's GUI.

Download

Get the latest PlugBrowser-<version>-win-x64.zip from the Releases page, unzip it anywhere, and run PlugBrowser.App.exe. Windows 10 or 11, 64-bit. Nothing else needs installing: .NET and the C++ runtime are included.

The download is not code-signed yet, so Windows SmartScreen may warn about an unrecognized app the first time it runs; choose More info → Run anyway.

Status

Discovery, catalog, and browser UI are complete. Scanning finds 418 plugins on the development machine (196 VST3, 222 VST2) in about 300 ms, without loading a single plugin.

Capture works, at scale. A full run over this machine's library with the native worker:

418 found · 194 loaded · 199 screenshots · 2 failed · 12 min

That is 99% of installed VST3 plugins photographed — effects, instruments and copy-protected titles alike — every one via PrintWindow off screen. That machine is a hybrid-graphics laptop, which is why: on a desktop GPU the same library lost all 87 Arturia editors to blank captures until the on-screen fallback below was added.

For comparison, the managed worker on the same library managed 190 loaded · 189 screenshots · 15 failed in 22 minutes. Most of that extra time was the 45-second timeout burned by each plugin it could not load.

Two plugins still fail: KORG MS-20 V dies with an access violation, and VocalSynth 2 stops responding. Both are contained by the sandbox and both are worth retrying — see the note on non-reproducibility below.

Those failures are also not reproducible run to run. The same bx_* plugin variously hangs until the watchdog kills it, dies by native fail-fast, or fails cleanly in 270ms with "load failed" — which is what licence-server interaction looks like from the outside. So a failed plugin is worth retrying; the catalog keeps the state and the reason so a later scan can pick it up.

Distinguishing the two cases needed care. The worker's watchdog ends the process with Environment.FailFast, and a plugin crashing via native __fastfail produces a nearly identical exit code, so a hang and a crash were indistinguishable from the parent. The watchdog now writes an explicit timed-out report before it pulls the plug, and the parent trusts that over the exit code.

Scanning is wired into the app. Scan & Options lists every scan root — conventional, registered by an installer, or added by you — with per-root enable switches and plugin counts, and runs the scan from the same dialog with live progress. Captured screenshots appear in the gallery at each plugin's own aspect ratio.

Filters, the chosen vendor, and window geometry are remembered between runs.

Browsing

  • Category facets built from the subcategory tags the plugins themselves report — Delay, Dynamics, Reverb, Modulation and so on, each with a count. Ticking several asks for any of them, which is how a facet list reads; requiring all would return almost nothing, since a plugin rarely claims two.
  • Every facet count cross-filters. Counts are computed against the rest of the active query, so typing "arturia" takes the sidebar from Delay (8) to Delay (3) and Drum (4) to Drum (0). Each group's count excludes its own selections — otherwise ticking Delay would show "Delay (1)" and zero beside every sibling, which is the opposite of what a facet list is for.
  • Result count above the gallery, tracking the current filter.
  • Full-size view: double-click a card, or click the detail pane's image. Click anywhere or press Escape to dismiss.
  • Clear filters resets everything including the search; the search box carries its own ✕.
  • Unique only hides plugins that are also installed in another format — see below.
  • Multi-select with the usual Ctrl/Shift gestures. The detail pane then shows only traits the selection genuinely shares — a differing one reports "(3 different)" rather than quietly showing the first plugin's value, which would be a claim about plugins it does not describe.

All detected plugins

Scan & Options → View all plugins opens every catalogued file as one spreadsheet-style grid: plugin, format, which formats it is installed as, type, vendor, version, architecture, size, whether it has a screenshot, and the binary's location. Every column sorts from its header. A search box matches the text columns, and each column with a fixed set of values (format, installed as, type, vendor, architecture, screenshot) has its own drop-down filter. Copy as spreadsheet puts the rows shown, in the order shown, on the clipboard as TSV, and Export CSV… saves the same rows to a file. Every column header has a tooltip explaining it.

Duplicates across formats

A VST2 and a VST3 build of the same plugin are usually installed together by one installer, so most catalogs contain each plugin twice. On the development machine that is 139 products and 278 files — 278 of 418 entries, leaving 140 unique. The grid's Installed as column shows it per row ("VST3 + VST2"); the Unique only facet hides those plugins in the main gallery, and Redundant only shows just them.

Matching is by a normalised product name, because the same product is named inconsistently across its own builds — "Addictive Drums 2" as VST3 against "Addictive Drums 2 x64" as VST2, "Comp FET-76" against "Comp FET 76". The normalisation is deliberately conservative: a false duplicate would invite someone to delete a plugin they still need, which is far worse than missing one. Audited against the real catalog, every group it found was a genuine match.

Reload & recapture

Some plugins show a registration or trial prompt in their editor, so the stored screenshot is a picture of that dialog rather than the interface — every Audio Plugin Union plugin on the development machine did this. Select those plugins and use Reload & recapture: it re-probes just that selection, bypassing the unchanged-file check, since nothing about the file changed — only the licence state outside it. A recapture that fails keeps the previous image rather than leaving the plugin with none.

Two VST3 subcategory groups are deliberately kept out of the category list. Fx and Instrument duplicate the Type facet, and listing them twice lets the two controls be set into a contradiction that silently returns nothing. OnlyRT, NoOfflineProcess and the channel-layout markers are processing capabilities the spec packs into the same string as musical categories — real, but noise in a list meant for picking "delay" or "reverb".

Layout

src/PlugBrowser.Core/    discovery, PE parsing, SQLite catalog, scan orchestration
native/                  PlugBrowser.NativeWorker - the C++ loader/capture worker (preferred)
src/PlugBrowser.Worker/  the managed worker, used as a fallback
src/PlugBrowser.App/     Avalonia 11 browser UI
tests/PlugBrowser.Tests/ xUnit suite
external/NetPlugHost/    the VST3 host (git submodule)
tools/                   make-icon.ps1, which regenerates the app icon from icon.png

Building from source

Needs the .NET 9 SDK and Visual Studio 2022 (or Build Tools) with the Desktop development with C++ workload, which brings MSVC and CMake.

git clone --recursive https://github.com/Bezmont/PlugBrowser.git
cd PlugBrowser
.\build-native.ps1              # NetPlugHost's native DLL and the native worker
dotnet build PlugBrowser.sln    # the app, with both workers copied next to it
dotnet test                     # the test suite
.\publish.ps1 -Version 1.0.0    # the release zip, under artifacts\

NetPlugHost, the VST3 host, is a git submodule at external/NetPlugHost, and it pins the Steinberg VST3 SDK in turn, hence --recursive. Already cloned without it? git submodule update --init --recursive. To build against a NetPlugHost checkout of your own instead, pass -NetPlugHost <path> to build-native.ps1 and -p:NetPlugHostDir=<path>\ to dotnet build.

Without the C++ workload the app still builds and catalogs plugins from their files, but cannot load them or take screenshots, since the VST3 host itself is native code.

Both native binaries link the C++ runtime statically, so the release runs on machines without the Visual C++ Redistributable.

Releases

.github/workflows/build.yml builds and tests every push. Pushing a version tag also packages the app and publishes a GitHub Release with the zip attached:

git tag v1.0.0
git push origin v1.0.0

A tag with a suffix, such as v1.1.0-beta.1, is published as a pre-release.

How capture works

The app never loads a plugin itself. PlugBrowser.Worker is launched once per bundle and killed — process tree and all — if it overruns its budget. Large sample-library and amp-sim plugins do hang on load; out of process that costs one catalog row marked "timed out" instead of a frozen UI.

Inside the worker, capture is: create an off-desktop top-level window, parent the plugin's editor into a child of it, pump messages until two consecutive captures come back identical (so splash screens and fades finish), then PrintWindow with PW_RENDERFULLCONTENT.

Two details that are easy to get wrong:

  • Re-query the editor size after attaching. Most plugins only know their real size then.
  • A blank capture is a silent failure, not an error. An OpenGL or Direct3D editor returns solid black and a success code, so the only way to detect it is to inspect the pixels — hence the distinct-colour check that rejects flat images.
  • Off screen is not the same everywhere. OpenGL lets a driver skip drawing pixels that are on no monitor. A hybrid laptop draws the whole frame anyway and copies it in, so the off-desktop window captures fine; a desktop GPU driving the display directly skips every pixel and the capture is blank. So a blank capture is retried with the window on the primary monitor, first as a click-through window at 1/255 opacity (OnScreenTransparent, invisible in practice), then fully visible (OnScreenVisible). On the desktop machine that recovered every blank editor: 88 through the transparent rung, and exactly one — Arturia's Bus PEAK — that drew only once the window was genuinely visible, which is what keeps the last rung in the ladder. --onscreen makes the native worker skip straight to those for testing.

Copy-protected plugins and the CLR

Plugin Alliance's bx_* titles, elysia, SPL and Knifonium hang forever in the .NET worker while loading fine in any DAW. The cause is not the plugin or the ABI: NetPlugHost's own native C++ smoke host loads every one of them in 2–5 seconds. The only difference is a CLR in the process.

During a hang the process burns a full core, has WININET/CRYPT32/WS2_32 loaded and no network connections — copy protection spinning in a local check loop, not waiting on a licence server. These titles use PACE/iLok anti-tamper, which probes for debuggers and instrumentation; a CLR process presents exactly that shape (vectored exception handlers, hardware exceptions for null checks, threads suspended for GC), so the protection goes defensive. It also explains the nondeterminism — the same plugin variously hangs, fails fast, or fails cleanly depending on how the race falls.

Fixed by PlugBrowser.NativeWorker, a C++ worker with no runtime in its process. All eight previously-unloadable plugins now load and capture in 3–7 seconds:

bx_solo               Fx|Analyzer        7 params    380x114
bx_subfilter          Fx|EQ|Filter       8 params    481x211
bx_masterdesk Classic Fx|EQ|Dynamics    12 params    630x389
bx_console N          Fx|EQ|Dynamics    55 params    700x785
bx_oberhausen         Instrument|Synth 269 params    850x683
elysia niveau filter  Fx|EQ              5 params    455x174
SPL Free Ranger       Fx|EQ              8 params    308x450
Knif Audio Knifonium  Instrument|Synth 317 params   1200x541

The worker talks to plugins through NetPlugHost's flat C ABI rather than the VST3 SDK directly, so all the hosting logic stays in one place and the worker stays a thin shell around windowing, capture and JSON — no SDK headers, no submodule, three source files. It emits the same result.json the managed worker does, so nothing downstream changed.

WorkerRunner picks the native worker whenever it is present and falls back to the managed one otherwise, because building it needs a C++ toolchain.

The host must supply an IPlugFrame

The VST3 spec requires IPlugView::setFrame(IPlugFrame*) before attached(). NetPlugHost never called it, and the omission is not benign: plugins are entitled to call plugFrame->resizeView() from inside attached() when their editor sizes or scales itself, and one that does not null-check its frame dereferences null and takes the host process down.

That is what every Native Instruments plugin was doing. Battery 4 and Kontakt died inside attached() with NI's own crash dialog; Super 8 produced a bare 0xC0000005 access violation. All of them load fine in any DAW, because every DAW implements this interface. Fixed in NetPlugHost by implementing a minimal IPlugFrame that resizes the parent window and forwards to onSize().

One wrinkle: the SDK declares IPlugFrame's IID but defines it in none of the translation units this target compiles, so the host has to provide it with DEF_CLASS_IID(IPlugFrame).

Known limitation. A few plugins draw an information or licence screen inside their own editor rather than in a separate window — every Elektron Overbridge plugin does this — so the capture is of that notice, not the interface. Foreign-window detection cannot catch it, because there is no foreign window. These are visible in the gallery and can be recaptured individually.

Reported editor size is not always the size drawn: Arturia's Comp FET-76 reports 960x508, the height it needs with its "Advanced" panel open, but paints only the top 289px while collapsed. Uniform edge bands are cropped off, so stored images match the visible interface and the gallery can lay every card out at the plugin's true proportions.

Running

dotnet test                                  # the test suite
dotnet run --project src/PlugBrowser.App     # the browser

The catalog lives at %LOCALAPPDATA%\PlugBrowser\catalog.db. Deleting it forces a full rescan.

Every scan and reload appends to %LOCALAPPDATA%\PlugBrowser\scan.log: each bundle's outcome, every class's editor size and capture method, the reason any screenshot was not taken, and whatever the plugin printed to the console. It rotates to scan.previous.log past 4 MB. Start there when a plugin has no picture.

How discovery works

No plugin is loaded. Everything shown today comes from the filesystem:

  • Traversal. A .vst3 entry is one plugin whether it is a file or a directory, and a .vst3 directory is never descended into — doing so rediscovers its own Contents/x86_64-win/Name.vst3 binary as a phantom second plugin.
  • PE version resource. The dependable source of vendor and version. On this machine 147 of 151 flat VST3 files carry one, and VST2 DLLs carry product names too.
  • Exports. GetPluginFactory marks VST3, VSTPluginMain/main marks VST2 — but the file extension must be part of the test, because every Arturia VST3 also exports main.
  • moduleinfo.json. Rich when present (CIDs, categories, SDK version) but rare: 3 bundles of ~229. Note the SDK writes "Sub Categories" with a space and emits trailing commas.
  • Vendor snapshots. Contents/Resources/Snapshots/<CID>_snapshot.png is part of the VST3 spec and would be the ideal plugin image — but zero plugins on this machine ship one, so it is a bonus path, not a foundation.

Notes on the VST3 host

Plugins are addressed by bundle path and factory index. Exposing each class's CID through NetPlugHost would let them be addressed by identity instead: index-based addressing can re-point cached data at the wrong plugin when a vendor ships an update that reorders a bundle's classes. Worth doing, but not blocking, since nearly every bundle exposes exactly one class.

Instruments are not invisible

An earlier reading of NetPlugHost suggested its kVstAudioEffectClass filter hid instruments, making the rebuild urgent. That is wrong, and worth recording so nobody re-derives it: in VST3, kVstAudioEffectClass is the component category for every audio processor. Instruments included. The instrument/effect distinction lives in the subcategory string — "Fx|Dynamics" versus "Instrument|Synth" — which the v1 ABI already reports. Instruments enumerate, instantiate and screenshot today.

Related repositories

  • NetPlugHost is the VST3 host PlugBrowser loads plugins through: a C++ wrapper around the Steinberg VST3 SDK with a C# P/Invoke layer. It loads modules, instantiates plugins, and opens their editors into a host-supplied window, which is the hard half of screenshot capture. It is included here as a submodule.
  • StreamRecorder uses the same host, which is why NetPlugHost is only ever extended additively: its existing exports keep their exact meaning, and new ones sit alongside them.

Licensing

GPLv3; see LICENSE. NetPlugHost is GPLv3 because it links the Steinberg VST3 SDK under that SDK's GPLv3 option, and linking NetPlugHost makes PlugBrowser GPLv3 too.

VST is a registered trademark of Steinberg Media Technologies GmbH.

VST2

VST2 is catalogued but never loaded, and gets no screenshots. Steinberg no longer licenses the VST2 SDK, so hosting it would require a clean-room header. The metadata available without loading turns out to be good — vendor, product name, version, architecture — so VST2 entries are not second-class in the browser.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages