From pulp
Helps wire AUv2 plug-in adapter code in Pulp, focusing on component type selection (aufx/aumf/aumi/aumu), MIDI routing, and avoiding silent DAW-side cache failures.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pulp:auv2The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this when you are:
Use this when you are:
core/format/src/au_v2_adapter.cpp / .hpp or the AU v2 instrument adaptertools/cmake/PulpUtils.cmake, tools/cmake/PulpInfoPlist.au.in)type in the Info.plist)AUMIDIBase, MIDIOutput, etc.)Scope is AU v2 only. AU v3 (AUAudioUnit-based app extensions) has different rules and lives behind the ios skill + core/format/src/au_adapter.mm.
Hosts route MIDI based on the bundle's type field. Getting this wrong produces a silent-failure: the plug-in scans, loads, renders audio, and never sees a MIDI event.
type | Constant | Audio I/O | MIDI I/O | When to use |
|---|---|---|---|---|
aufx | kAudioUnitType_Effect | in + out | none | Audio-only effect (compressor, EQ, reverb) |
aumf | kAudioUnitType_MusicEffect | in + out | in | Effect that wants inbound MIDI (arpeggiator on audio, MIDI-triggered gate, vocoder w/ MIDI carrier) |
aumi | kAudioUnitType_MIDIProcessor | none | in + out | MIDI-only processor (transpose, arp, chord, note filter) |
aumu | kAudioUnitType_MusicDevice | out only | in | Instrument / synth |
Load-bearing rule: Logic, MainStage, GarageBand, and other AU hosts will never call MIDIEvent / HandleMIDIEvent on an aufx-typed plug-in. If a plug-in's Processor::descriptor() sets accepts_midi = true and the bundle is still packaged as aufx, MIDI silently disappears — the adapter looks correct, the Info.plist looks correct to a casual reader, but no MIDI arrives.
Pulp's pulp_add_plugin() automates the choice from two inputs:
CATEGORY — Effect | Instrument | MidiEffectACCEPTS_MIDI — bool option that mirrors PluginDescriptor::accepts_midiThe resulting mapping (_pulp_add_au / _pulp_add_auv3 in tools/cmake/PulpUtils.cmake):
(Instrument, *) -> aumu
(MidiEffect, *) -> aumi
(Effect, true) -> aumf <-- easy to forget
(Effect, false) -> aufx
When you add a new example or change an existing one's descriptor to declare accepts_midi = true, you must also add ACCEPTS_MIDI to its pulp_add_plugin() call. There is no runtime fallback — the two surfaces are independent and the CMake flag is what ends up in the Info.plist.
Two independent things must both be true for an aumf to receive MIDI, and it is easy to get the first and miss the second:
PulpAUEffect derives AUMIDIEffectBase and overrides HandleMIDIEvent / HandleSysEx. ✅ (always true for Pulp effects)ausdk::AUBaseFactory (AUBaseLookup) has no MusicDevice selectors, so even though the class implements HandleMIDIEvent, the host's MusicDeviceMIDIEvent call returns -4 (unimpErr) — no note ever reaches the adapter, and auval -v aumf fails with -4 IN CALL MusicDeviceMIDIEvent (often with a cascading Bad Max Frames line that clears once the MIDI dispatch is fixed).So the entry macro is type-specific (core/format/include/pulp/format/au_v2_entry.hpp):
| Macro | Factory | Use for |
|---|---|---|
PULP_AU_PLUGIN | ausdk::AUBaseFactory | aufx (audio-only effect) |
PULP_AU_MIDI_PLUGIN | ausdk::AUMIDIEffectFactory (AUMIDILookup = MIDIEvent + SysEx) | aumf (MIDI-receiving effect) |
instrument entry (au_v2_instrument_entry.hpp) | ausdk::AUMusicDeviceFactory (AUMusicLookup = + StartNote/StopNote) | aumu (instrument) |
Three surfaces must agree for an aumf, or the component is invalid: descriptor().accepts_midi = true, the aumf type (CMake ACCEPTS_MIDI or a hand-written Info.plist.au), and PULP_AU_MIDI_PLUGIN in the plugin's au_v2_entry.cpp. The dispatch contract is pinned by test/test_au_v2_effect.cpp ([dispatch] — asserts AUMIDILookup routes kMusicDeviceMIDIEventSelect and AUBaseLookup does not), so a regression to the base factory fails in CI instead of at auval/Logic time.
The aumu instrument variant is the same trap (seen with PulpTempoSampler): a category = Instrument plugin gets an aumu Info.plist from CMake, but if its au_v2_entry.cpp uses PULP_AU_PLUGIN (→ AUBaseFactory) instead of PULP_AU_INSTRUMENT (#include <pulp/format/au_v2_instrument_entry.hpp> → AUMusicDeviceFactory/AUMusicLookup), the host's MusicDeviceMIDIEvent returns -4. The plugin loads and plays UI-triggered audio (slice taps, on-screen keyboard — those bypass host MIDI via the UI→audio queue), so it looks fine, but silently ignores all host MIDI in Logic/Ableton. auval -v aumu catches it (Test MIDI fails); the same [dispatch] test now also pins AUMusicLookup carries the selector and AUBaseLookup does not. The factory name (<ClassName>Factory) is identical across both macros, so swapping PULP_AU_PLUGIN → PULP_AU_INSTRUMENT keeps the generated Info.plist factory reference valid.
The adapter inherits AUMIDIEffectBase (AUEffectBase + AUMIDIBase) so the SDK's MIDIEvent / SysEx entry points exist. Inbound MIDI flows:
host -> AUMIDIBase::MIDIEvent(status, data1, data2, frame)
-> AUMIDIBase::HandleMIDIEvent(strippedStatus, channel, data1, data2, frame)
-> PulpAUEffect::HandleMIDIEvent(...) <-- our override
- lock midi_mutex_
- push MidiEvent into pending_midi_
At the top of ProcessBufferLists() we drain under the same lock:
lock midi_mutex_
midi_in = std::move(pending_midi_)
pending_midi_ = {}
unlock
midi_in.sort() // sample-accurate ordering
processor_->process(..., midi_in, midi_out, ctx)
The instrument adapter (core/format/src/au_v2_instrument.cpp) uses the same pending_midi_ + midi_mutex_ pattern against the MusicDeviceBase base class. If you're adding a third MIDI-aware AU, mirror that shape exactly — resist the urge to share a mixin until there's a third entry to fold.
decode_midi_event()AUMIDIBase::HandleMIDIEvent delivers the status byte already split into a top nibble and a separate channel. The free function pulp::format::au::decode_midi_event(status, channel, data1, data2) in core/format/include/pulp/format/au_v2_adapter.hpp recombines them into a choc::midi::ShortMessage with the correct on-the-wire status byte and returns a MidiEvent with sample_offset == 0. Tests cover CC, pitch bend, note-on, program change, and system messages (status 0xF0+ keep their literal byte — channel nibble is ignored).
AUMIDIBase::HandleSysEx(data, length) does not carry a per-event sample offset at this SDK layer. We enqueue the payload with sample_offset == 0 so it is delivered at the leading edge of the current ProcessBufferLists() block.
ProcessBufferLists() now set_param_events(¶m_events_) before
processor_->process(...) and wraps ONLY the process call in
pulp::runtime::ScopedNoAlloc (the preamble — param snapshot, pointer-vector
resizes — legitimately allocates, so don't widen the guard). This makes the
param-events contract uniform across formats (VST3/CLAP/AUv3 already had it).
AU v2's AUEffectBase has no scheduled/ramped parameter event source, so
param_events_ is empty — host params still reach the Processor through
store_ (StateStore) exactly as before. A native-component (NativeCoreProcessor)
plugin therefore won't receive sample-accurate params on AU v2 yet; that needs
the AUv3 AURenderEventParameter model and is a follow-up. Do not synthesise an
AU v2 param-event mapping by guessing.
ProcessBufferLists() now wraps the current main input/output buffers in a
stack-owned ProcessBuffers block and calls the additive
processor_->process(process_buffers, midi_in, midi_out, ctx) overload inside
the existing ScopedNoAlloc guard. Legacy processors still reach the original
main-in/main-out callback through the default projection, while processors that
override the richer overload can inspect AU v2 bus metadata directly. The AU v2
instrument Render() path uses the same additive dispatch with an inactive,
optional main input bus and the active output bus.
A Processor flags a mid-render latency or tail change via
flag_latency_changed() / flag_tail_changed() (RT-safe atomic
store-release). Never call AU SDK property-change APIs from
process() directly — the adapter owns the host-thread publish path.
AU v2 wiring (post-process): the adapter checks the consume helpers
and calls PropertyChanged(kAudioUnitProperty_Latency) /
PropertyChanged(kAudioUnitProperty_TailTime). The AU v2 SDK queues
these for delivery on the main thread, so it's safe to invoke from
the audio callback path. Tests live in
pulp-test-processor-layout-latency plus the existing
pulp-test-au-v2-effect suite.
MIDI output from AU v2 effects is not wired yet; #626 tracks the missing render-notify / MIDIOutput path. Processor::process() can write to midi_out, but PulpAUEffect has no render-notify callback / MIDIOutput mixin that emits those events back to the host. Effects that declare produces_midi = true work in CLAP / VST3 but stay silent on AU v2. descriptor.produces_midi is not wired to a CMake flag yet — the AU type selection is driven entirely by accepts_midi.
AU v3 parity for MIDI on effects is not re-audited in this pass. If you touch core/format/src/au_adapter.mm, confirm the AUv3 componentType logic in _pulp_add_auv3 still matches the fix in _pulp_add_au.
type changeLogic, MainStage, GarageBand, Studio One, Live, and every other AU host maintain a host-side cache of AU descriptors, keyed on subtype + manufacturer. When you change a plug-in from aufx to aumf (or vice versa) without also changing the subtype, hosts will keep the cached-old-type descriptor and behave as if the fix never shipped — you'll install a fresh .component and the host will still treat it as aufx. Symptoms: rebuilt plug-in appears in the correct MIDI-effect slot of the host UI only after a restart, or never appears at all.
Mitigation when you test a type change locally:
# Kill the AU registration cache so the next host launch re-inspects the bundle.
killall -9 AudioComponentRegistrar 2>/dev/null || true
# Logic / MainStage / GarageBand — clear the AU cache next to the host DBs.
rm -rf ~/Library/Caches/AudioUnitCache
rm -rf ~/Library/Caches/com.apple.audiounits.cache
# auval rescan catches the new type without needing a host restart.
auval -a | grep <subtype>
auval -v <type> <subtype> <manufacturer>
Document this step in any issue or PR that flips a shipped plug-in's component type.
Beware the transient false PASS. Right after killall AudioComponentRegistrar
the daemon is re-inspecting every component, and auval run during that window
returns flickering results — it can report PASS once, then FAIL (or the
"didn't find the component" error) on the next run, against the same bundle. A
type flip burned real time here: a mid-rescan PASS looked like the fix worked,
but the stable result was FAIL. Always let the rescan settle (sleep 4-5)
and run auval at least twice, and only trust a result that is stable across
runs. A single green run immediately after a cache kill is not a pass.
auval tests on persistent runners — kill the cache before every runSelf-hosted CI runners (and local dev iteration where the same plug-in is
rebuilt repeatedly) hit the same AudioComponentRegistrar cache that
hosts use. Even with the .auvaltest.component rename trick (copy to a
suffixed path to avoid the canonical .component collision), the cache is
keyed by bundle ID, so a stale entry from the previously-installed
canonical bundle survives. auval then reports:
ERROR: Cannot get Component's Name strings
ERROR: Error from retrieving Component Version: -50
* * FAIL
FATAL ERROR: didn't find the component
even though the freshly-copied bundle is well-formed (nm shows the AU
factory symbol, plutil -p Info.plist is valid, codesign -dv succeeds).
On a fresh machine the test passes; on a persistent runner it
intermittently fails.
The fix is one line in the auval ctest command — kill the registrar
between install and validation:
add_test(NAME auval-MyPlugin
COMMAND bash -c "d=\"$HOME/Library/Audio/Plug-Ins/Components/MyPlugin.auvaltest.component\"; \\
rm -rf \"$d\"; \\
cp -R \"${CMAKE_BINARY_DIR}/AU/MyPlugin.component\" \"$d\" && { \\
killall -KILL AudioComponentRegistrar 2>/dev/null || true; \\
sleep 1; \\
auval -v aufx MyFx Pulp 2>&1 | tee /dev/stderr | grep -q 'PASS'; \\
}; \\
rc=\$?; rm -rf \"$d\"; exit \$rc")
|| true prevents set -e exit when no registrar is running; sleep 1
gives macOS time to relaunch the daemon before auval queries it. The
PulpEffect/PulpGain/PulpTone/PulpPluck examples all use this pattern.
ChainerSynth doesn't need it because its aumu Chnr codes are first-time
unique on the runner, but any new aufx/aumu/aumf plug-in sharing a
manufacturer+subtype pattern with an existing test should add the cache
kill.
Surface symptom matches the host-cache one above; the difference is
you can't rely on .auvaltest.component alone to defeat it.
AUEffectBase vs AUMIDIEffectBaseIf you see HandleMIDIEvent that never fires: check the base class. AUEffectBase alone has no AUMIDIBase mixin — the SDK only wires MIDIEvent dispatch when the class multiply inherits AUMIDIBase (directly or via AUMIDIEffectBase / MusicDeviceBase). When you add a new AU v2 adapter, inheriting from AUMIDIEffectBase is cheap even for audio-only effects — the class does nothing extra until the host actually delivers MIDI, and it future-proofs the adapter against a later accepts_midi flip.
GetProperty / GetPropertyInfo chainWith AUMIDIEffectBase, fall-through calls should go to AUMIDIEffectBase::GetProperty(...), not AUEffectBase::GetProperty(...). AUMIDIEffectBase::GetProperty tries AUEffectBase::GetProperty first and then falls back to AUMIDIBase::DelegateGetProperty. Calling AUEffectBase directly skips the MIDI-mapping property delegation — hosts that query kAudioUnitProperty_AllParameterMIDIMappings would silently return no mapping.
core/format/include/pulp/format/au_v2_adapter.hpp pulls AudioUnitSDK/AUMIDIEffectBase.h, which on AudioUnitSDK 1.4 uses std::expected (C++23). Apple clang only exposes std::expected when the consuming TU compiles at -std=c++23. Any test executable that includes the adapter header must set CXX_STANDARD 23 explicitly — linking pulp::format is not enough because CMake treats CMAKE_CXX_STANDARD=20 at the root as authoritative per target. See core/format/CMakeLists.txt for the equivalent pin.
pending_midi_ mutex is a slow-path correctness tool, not a fast pathThe std::mutex guarding pending_midi_ is contended only on the MIDI-delivery thread (where the host calls HandleMIDIEvent) and the audio thread (once per block, to drain). It is NOT the right primitive for per-event audio-thread publication. Do not extend this pattern to any new path that runs multiple times per block — switch to choc::fifo::SingleReaderSingleWriterFIFO if you need lock-free MIDI delivery inside a single block.
AUMIDIBase splits the status byte for EVERY messageAUMIDIBase::MIDIEvent (AudioUnitSDK 1.4 AUMIDIBase.h) unconditionally splits the wire-format status byte before dispatching:
strippedStatus = inStatus & 0xF0 // -> HandleMIDIEvent's inStatus
channel = inStatus & 0x0F // -> HandleMIDIEvent's inChannel
The split happens for system messages (0xF0-0xFF) the same way as for channel-voice (0x80-0xEF). For 0xF8 (timing clock) the SDK calls HandleMIDIEvent(inStatus=0xF0, inChannel=0x08, ...). The decoder MUST reassemble (inStatus & 0xF0) | (inChannel & 0x0F) regardless of the top nibble — special-casing system messages and returning inStatus unchanged turns every clock / start / stop / song-position into 0xF0 (sysex start). The unit test in test/test_au_v2_effect.cpp now feeds the post-split shape (status=0xF0, channel=0x08) so the regression cannot reappear without flipping a test red.
AUSDK_RTSAFE position with override — Xcode 16.4 incompatAUSDK_RTSAFE expands to [[clang::nonblocking]]. AudioUnitSDK's own base-class declarations use ... AUSDK_RTSAFE; (no override), but placing the attribute between a function declarator and the override virt-specifier in a derived class compiles under older Xcode and fails on Xcode 16.4 / Clang 17+ with:
error: expected ';' at end of declaration list
The attribute is a static-analysis hint only — dropping it from derived-class override declarations has no runtime effect. PulpAUInstrument::HandleNoteOn/Off (the reference pattern for AU v2) doesn't carry AUSDK_RTSAFE either. When writing a new AU v2 override that matches an AUSDK_RTSAFE base declaration, omit the attribute. This incompatibility surfaces on CI's Coverage-macOS leg.
dealloc ordering — never call bridge->close() explicitlyPulpAUEditorOwnership (in core/format/src/au_v2_cocoa_view.mm) declares its members as unique_ptr<ViewBridge> bridge then unique_ptr<PluginViewHost> host. C++ destroys members in REVERSE declaration order, so when delete _ownership runs in PulpAUEditorOwner::dealloc:
~PluginViewHost runs first. The host calls root_.set_plugin_view_host(nullptr) to clear the View → host back-pointer. The View it references is still alive (still owned by bridge->view_), so the call is safe.~ViewBridge runs second. Its destructor calls close() → Processor::on_view_closed(*view_raw_) fires → view_.reset() destroys the View. The back-pointer was already cleared in step 1, so the View's own teardown can't reach a dead host.Calling _ownership->bridge->close() HERE explicitly (BEFORE delete _ownership) reverses that order: the View dies first, then ~PluginViewHost dereferences a dangling root_ reference and crashes the AU v2 editor close path. The fix is to remove the explicit close, NOT to add it. Same rule applies to any future Cocoa-View ownership wrapper that mixes a ViewBridge and a PluginViewHost in the same C++ scope.
use_gpuau_v2_cocoa_view.mm no longer sets Options::use_gpu by hand; it calls
pulp::format::decide_gpu_host(*bridge) so a Skia/Dawn/scripted editor gets the
GPU PluginViewHost automatically (hardcoding use_gpu=false was the bug that
made it fall back to AutoUi/CPU). It also wires host->set_resize_callback(...)
because AU v2 has no host size callback — the DAW resizes the returned
NSView directly, so native frame changes are forwarded to bridge->resize()
through that seam. Full contract: the view-bridge skill's "GPU view host
auto-selection" section.
Selecting the GPU host (above) is necessary but NOT sufficient. The host only
loads the Pulp editor if the AU advertises kAudioUnitProperty_CocoaUI. For a
long time NO Pulp AU v2 did — fill_cocoa_view_info() existed but was never
wired into GetProperty, so Logic/auval saw Cocoa Views Available: 0 and fell
back to a generic param view (the symptom: a plain "Level" slider instead of the
real editor). Both PulpAUEffect and PulpAUInstrument now serve
kAudioUnitProperty_CocoaUI in GetProperty/GetPropertyInfo. Watch-outs:
pulp-format lib without PULP_AU_GUI,
while the Cocoa view module (au_v2_cocoa_view.mm) is added per-*_AU target
with it. So an #ifdef PULP_AU_GUI in the adapter is always off. The view
is reached via a runtime hook g_cocoa_view_info_filler (hidden visibility,
defined in au_v2_adapter.cpp) that the view module's static-init registers.
Query it ungated; null → delegate to base (no view).PulpAUInstrument, MusicDeviceBase) must ALSO serve
kPulpEditorContextProperty — the Cocoa view factory reads it to reach the
Processor + StateStore. It originally overrode no GetProperty at all.CFBundleCopyBundleURL PAC-crashes (__CFCheckCFInfoPACSignature,
PAC_EXCEPTION/SIGKILL) inside pointer-auth-hardened sandboxed hosts (Logic's
AUHostingServiceXPC, auval) the instant the view is queried — a hardware trap
a @try cannot catch. Use -[NSBundle bundleURL] instead. This was the actual
reason the editor never loaded even in code paths that tried.PULP_AU_COCOA_VIEW_CLASS,
injected per *_AU target from MFR+CODE 4ccs). ObjC class names are
process-global; two Pulp AUs in one host would collide on a fixed name.auval -v → expect Cocoa Views Available: 1. Covered by
test/test_au_v2_cocoa_ui.mm.auval automation must disable editor creationauval can instantiate AU editor surfaces during validation, which is
not acceptable in CI, headless tests, or local agent runs. CTest/CLI
validator paths must carry
PULP_DISABLE_PLUGIN_EDITOR=1 PULP_HEADLESS=1 PULP_TEST_MODE=1; the AU
Cocoa view factory returns nil under those guards. Keep this
environment on every auval-* test even if the validator command itself
looks audio-only.
The adapter overrides GetParameter/SetParameter to read/write the plugin's
StateStore directly. The host's parameter value IS the store value — there is
NO separate Globals()/AUElement copy to reconcile each block. Do not
reintroduce a per-block GetParameter()→store pull: it reverts UI-thread edits
(XY snap-back, type-in not taking) on the very next block, because the editor
writes the store but not the host cache. The render thread must perform NO
host-parameter write or notification — AUEventListenerNotify /
AUBase::SetParameter / Globals()->SetParameter from ProcessBufferLists
reentrantly stalls Logic's render thread and silences audio. UI edits reach the
host via the gesture begin/end brackets (set_gesture_callbacks, UI thread) and
an Audio-thread store listener that notifies on the editing thread with a
thread_local echo guard so a host-originated SetParameter is not echoed back.
The instrument adapter (au_v2_instrument.cpp) now wires the same
set_gesture_callbacks block (it previously only had the value-change
listener, so slider drags in an instrument editor recorded values but
never bracketed them with kAudioUnitEvent_BeginParameterChangeGesture /
…EndParameterChangeGesture — Logic would not arm a write pass). Mirror
au_v2_adapter.cpp's gesture block exactly: emit Begin on
begin_gesture, End on end_gesture, both via AUEventListenerNotify
with the g_host_writing_param echo guard.
HandleMIDIEvent/HandleSysEx push to lock-free SpscQueue<MidiEvent> +
bounded SysexChunk queues; ProcessBufferLists drains them wait-free. Don't
add a std::mutex to the MIDI path — short messages stay allocation-free and
the audio thread never blocks. The AU v2 instrument adapter
(au_v2_instrument.cpp) uses the same single-source params + SpscQueue
note-input pattern (HandleNoteOn/HandleNoteOff → lock-free queue).
core/format/src/au_v2_adapter.cpp, core/format/include/pulp/format/au_v2_adapter.hppcore/format/src/au_v2_instrument.cpp, core/format/include/pulp/format/au_v2_instrument.hppcore/format/src/au_v2_cocoa_view.mm (owned by view-bridge + ios skills)tools/cmake/PulpUtils.cmake — _pulp_add_au and _pulp_add_auv3tools/cmake/PulpInfoPlist.au.inexternal/AudioUnitSDK/include/AudioUnitSDK/AUMIDIBase.h, AUMIDIEffectBase.hdocs/status/support-matrix.yaml — formats.au_v2 and format_limitations.au_v2test/test_au_v2_effect.cpp — decode / sysex smoketest/cmake/test_au_v2_type_selection.cmake — aumf/aufx/aumu/aumi mappingau_v2_cocoa_view.mm now calls
bridge->scripted_ui()->attach_gpu_surface(host->gpu_surface()) right
after PluginViewHost::create() succeeds. Skip this and an AU v2
plugin whose UI uses Three.js or raw WebGPU JS renders black — the JS
shim silently falls back to mocks. See the view-bridge skill's
"GpuSurface plumbing into WidgetBridge" section for the cross-platform
contract.
This adapter consumes the host-quirks ledger at init: it caches
resolved_quirks(detect_host_info().type, version) once (the runtime
policy — PULP_HOST_QUIRKS env / set_host_quirk_policy() API / compile
default — applies via resolved_quirks()), then gates DAW accommodations
on those flags instead of hardcoding them.
First wired flag: clamp_latency_to_nonneg. Latency reporting routes
through the pure helper pulp::format::reported_latency_samples(raw, quirks)
(in host_quirks.hpp): a negative latency_samples() clamps to 0 when the
quirk is enforced, and passes through raw (wrapping the unsigned host field)
when PULP_HOST_QUIRKS=off. See docs/reference/host-quirks-policy.md.
This adapter synthesizes the host-quirks bypass parameter and short-circuits the
process path when bypass is active.
At init (clap_init / PulpAUEffect ctor) it calls
pulp::format::maybe_synthesize_bypass(store, host_quirks) then detects the
"Bypass" param (shared boolean-range heuristic: name=="Bypass", step>=1,
0..1) into a cached bypass_param_id. In the audio callback (clap_process /
ProcessBufferLists) it short-circuits to a null-guarded pass-through
(copy main input → output, zero any output channel without a matching input)
and skips the Processor when the param value is >= 0.5 — mirroring the VST3
processBlockBypassed path. PULP_HOST_QUIRKS=off synthesizes nothing
(bypass_param_id stays 0). The pass-through MUST null-check each destination
channel pointer (a bus can report channels with null buffers).
PulpAUInstrument::GetLatency() now routes
the processor's latency through reported_latency_samples() (clamped,
policy-gated) instead of hardcoding 0.0 — instruments with lookahead get
host PDC. MusicDeviceBase has no GetSampleRate(); read it from
GetOutput(0)->GetStreamFormat().mSampleRate (guarded for pre-config).ProcessBufferLists bypass
short-circuit now drains + DISCARDS pending_midi_ under midi_mutex_
before returning. Without it, MIDI received while bypassed accumulated and
flooded the processor with stale notes/CCs the instant bypass turned off.
A bypassed plugin is a wire — inbound MIDI is dropped with the block.npx claudepluginhub danielraffel/pulp --plugin pulpValidates AudioUnit v2/v3 plugins (.component bundles and .appex app extensions) on macOS using Apple's auval tool, including registration and rescan steps.
Integrates Visage GPU-accelerated UI framework into JUCE audio plugins for macOS and iOS/iPadOS. Covers Metal view embedding, event bridging, focus management, keyboard handling, popups, memory management, and Visage API reference.