Skip to content

Version 0.16.0 - #504

Merged
defiantnerd merged 12 commits into
mainfrom
next
Aug 8, 2026
Merged

Version 0.16.0#504
defiantnerd merged 12 commits into
mainfrom
next

Conversation

@defiantnerd

Copy link
Copy Markdown
Collaborator

Release 0.16.0: merge next into main

defiantnerd and others added 12 commits July 21, 2026 09:06
This matches the timer object in the VST3 SDK which has a similar purpose.
It prevents hosts from behaving badly when not initializing a plugin correctly.
* AUv2: full MIDI 1.0 + MIDI 2.0 (UMP) in/out, note-only init, contract fixes

Overhaul MIDI handling in the AUv2 wrapper so it reaches parity with the
VST3 backend for MIDI 1.0 and adds MIDI 2.0 / Universal MIDI Packet support
in both directions. All MIDI 2.0 paths are guarded by AUSDK_MIDI2_AVAILABLE.

Dialect handling:
- Replace the single preferred==CLAP test with a real switch over the input
  port's preferred_dialect, validated against its supported_dialects with a
  MIDI2->MPE->MIDI->CLAP fallback (chooseInputDialect). Both dialects are
  threaded through ProcessAdapter::setupProcessing.

MIDI 1.0 (input):
- Poly key pressure -> CLAP_NOTE_EXPRESSION_PRESSURE on the CLAP dialect
  (raw MIDI otherwise); CC/PC/channel-pressure/pitch-bend stay raw MIDI.
- Implement SysEx input (WrapAsAUV2::SysEx) via a per-cycle owned buffer pool
  so the borrowed clap_event_midi_sysex_t.buffer stays valid.
- StartNote/StopNote now emit real CLAP notes carrying a note_id (with the key
  embedded in its low 7 bits, so StopNote needs no lookup table) plus a tuning
  note-expression for fractional pitch.
- Scope active-note bookkeeping to the CLAP dialect (the note union fields are
  only valid there).

MIDI 1.0 (output):
- Forward NOTE_EXPRESSION (pressure -> poly aftertouch) and MIDI_SYSEX instead
  of dropping them; add MIDIOutput::addSysEx.
- Remove the bug that echoed incoming notes straight to the MIDI output.

MIDI 2.0 / UMP:
- Override AUBase::MIDIEventList: walk UMP packets, forwarding MIDI 2.0
  channel-voice (MT 0x4) as CLAP_EVENT_MIDI2 and MIDI 1.0 (MT 0x2) through the
  existing byte path; reassemble SysEx7 (MT 0x3) into CLAP SysEx events.
- Advertise/handle the protocol properties: AudioUnitMIDIProtocol (read,
  reports MIDI2 only when the plugin prefers it) and HostMIDIProtocol (write).
- Output via kAudioUnitProperty_MIDIOutputEventListCallback: MIDIOutput builds a
  parallel protocol-1.0 UMP list (incl. SysEx7); Render prefers the EventList
  block when the host set one, else the legacy MIDIPacketList callback. Raw
  CLAP_EVENT_MIDI2 output is down-converted to MIDI 1.0. Native MIDI1->MIDI2
  output upscaling is intentionally left to the framework's protocol conversion.

Wrapper fixes surfaced by clap-validator-plugins:
- Note-only / zero-audio-port plugins (aumi, or no audio-ports extension) now
  initialize: PostConstructor presents a placeholder silent output bus, and the
  plugin's real CLAP audio-port counts are passed to process() separately from
  the AU element counts (placeholder busses are zero-filled). Fixes Initialize
  failing with kAudioUnitErr_InvalidElement (-10877).
- Stop scanning the CLAP audio-ports extension while the plugin is active
  (CLAP AP01 contract violation): the port layout is snapshotted at
  PostConstructor and ValidFormat/SupportedNumChannels read the cache.

Also remove dead/placeholder code and tidy inconsistent property handling.

Verified: builds -Werror; auval clean (Validator Synth aumu + NoteFX aumi,
zero contract violations, Test MIDI PASS); MIDI in/out confirmed in Logic Pro.

* AUv2: MIDI review fixes — 10.13 availability, MIDI2 dialect, format handling

Follow-up to the AUv2 MIDI overhaul, addressing review findings.

- Guard every CoreMIDI EventList call with __builtin_available(macOS 11.0, *):
  AUSDK_MIDI2_AVAILABLE only reflects the SDK headers, not the deployment
  target (10.13), so MIDIEventListInit/Add were unavailable at runtime and
  broke the 10.13 build. On older systems the UMP list stays null and all UMP
  appends become no-ops.
- Guard MIDIEventListAdd against a null curPacket (list full, or never
  initialized pre-11) so a full/absent UMP list drops events instead of
  crashing.
- On UMP input, only forward MIDI 2.0 channel-voice (MT 0x4) raw as
  CLAP_EVENT_MIDI2 when the plugin's note port actually declared the MIDI2
  dialect; otherwise down-convert to MIDI 1.0 and reuse the dialect-aware byte
  path (covers hosts that ignore our advertised protocol). Moves
  midi2ChannelVoiceToMidi1 into auv2_base_classes.h as inline for reuse.
- ValidFormat now accepts any format on the placeholder silent output bus of a
  note-only plugin (empty output-port cache, element 0), so hosts can change
  sample rate / channel count on that bus.
- clang-format all touched files.

Verified: builds -Werror with -DCMAKE_OSX_DEPLOYMENT_TARGET=10.13; auval clean
(Validator Synth aumu + NoteFX aumi, zero contract violations, Test MIDI PASS).
Note: auval skips format/render for aumi, so the ValidFormat placeholder-bus
change still wants a Logic sample-rate check on a note-only plugin.

* cmake: make AAX an opt-in flavor consistent with AUv2/AUv3

The top-level convenience build created the AAX wrapper target whenever the
platform was capable (CLAP_WRAPPER_CAN_BUILD_AAX, always true on macOS), with
no opt-in flag. Configuring that target fetches the AAX SDK, so a build that
only asked for AUv2 (e.g. -DCLAP_WRAPPER_BUILD_AUV2=1) still downloaded the AAX
SDK when CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES was set.

Gate the AAX flavor behind CLAP_WRAPPER_BUILD_AAX, the same opt-in style as
CLAP_WRAPPER_BUILD_AUV2 / _AUV3, and only honor it where AAX is actually
supported (CLAP_WRAPPER_CAN_BUILD_AAX: macOS and non-ARM MSVC Windows, never
Linux). If AAX is requested on an unsupported platform, emit a CMake warning
and skip it instead of failing or silently ignoring the request.

Document CLAP_WRAPPER_BUILD_AAX in the options header.

Note: AAX is now off by default (opt-in) rather than built automatically on
capable platforms; a bare `cmake . -B build` now produces VST3 (+ any
explicitly requested flavors) but no AAX - which makes more sense..

* Shared MIDI kernel + AUv3 MIDI 1.0/2.0 feature parity with AUv2

Bring the AUv3 wrapper up to the AUv2 MIDI feature set by extracting the
format-level translation into a shared header and building out AUv3's missing
paths. This is feature parity, not an adapter rewrite: each backend keeps its
own ProcessAdapter, event storage, note-id policy and host I/O, and only calls
the shared kernel for the fiddly bit manipulation that must behave identically.

Shared kernel (new src/detail/shared/midi_translation.h, header-only, pure):
- chooseInputDialect, umpMessageWordCount, midi1ToUmpWord,
  midi2ChannelVoiceToMidi1, packSysEx7 (MT 0x3 SysEx7), and a SysEx7Reassembler.

AUv2:
- Route the UMP/dialect/SysEx7 helpers through the shared kernel (remove the
  local copies so the backends cannot drift).
- Adopt the canonical rich mapping to match AUv3: channel pressure -> PRESSURE
  and pitch bend -> TUNING (+/-2 semitones) note expressions on the CLAP
  dialect (CC and program change stay raw MIDI).

AUv3:
- Add a clap_event_midi2 union member and capture the input port's
  supported_dialects (_midi_understands_midi2).
- MIDI 2.0 / UMP input: implement the AURenderEventMIDIEventList case — forward
  MIDI 2.0 channel voice as CLAP_EVENT_MIDI2 when the plugin declares the MIDI2
  dialect, otherwise down-convert to MIDI 1.0 and reuse the dialect-aware byte
  path; reassemble UMP SysEx7 into CLAP SysEx events.
- MIDI 2.0 / UMP output: build a protocol-1.0 UMP MIDIEventList and deliver it
  via MIDIOutputEventListBlock when the host provides one (the framework
  up-converts to the negotiated protocol), else the legacy 3-byte block; also
  emit CLAP_EVENT_MIDI2 and SysEx output.
- Protocol negotiation: override audioUnitMIDIProtocol to advertise MIDI 2.0
  when the hosted plugin prefers the MIDI2 dialect; capture hostMIDIProtocol.
- Extract the inline MIDI 1.0 channel-voice mapping into translateMidi1Bytes,
  shared by the legacy AURenderEventMIDI path and the UMP MIDI 1.0 messages.
- All MIDI 2.0 usage is guarded by __builtin_available(macOS 12.0, iOS 15.0, *);
  the CoreMIDI EventList API is above the 10.13 / iOS 15 deployment floor.

Verified: AUv2 builds -Werror; the AUv3 appex compiles, links and codesigns via
the Xcode generator at -target arm64-apple-macos10.13 (both process.mm and
auv3_audiounit.mm). Runtime unverified: the AUv2 channel-pressure/pitch-bend
mapping wants a Logic check, and the AUv3 UMP paths want a MIDI 2.0 host.

* MIDI: review fixes for the shared kernel + AUv3 parity work

Post-review hardening of the shared MIDI kernel and the AUv2/AUv3 MIDI paths.
Addresses correctness, real-time-safety and lifetime issues found reviewing the
previous commit; no intended behavior change beyond the bug fixes and the
AUv2 note-expression output that was previously incomplete.

Real-time safety / lifetime:
- MIDIOutput (AUv2): guard addNoteOn/addNoteOff/addMIDI3Byte/addSysEx against a
  null _current — once the 2048-byte MIDIPacketList fills, MIDIPacketListAdd
  returns null and must never be called again with a null curPacket.
- AUv3 SysEx output was a use-after-free: the plugin only guarantees the sysex
  buffer during try_push, but _outevents is drained after process() returns.
  Copy the payload into an owning buffer at enqueue time.
- Event-list output block (AUv2): a host can install/replace/clear the block
  while Render is invoking it on the audio thread. Make the block atomic, park
  replaced blocks in a retired-list instead of releasing them under the render
  thread, and release those in deactivateCLAP()/~WrapAsAUV2. deactivateCLAP now
  also clears the block so a stale one cannot shadow a legacy callback installed
  on the next initialization.
- Replace the per-cycle std::vector<std::vector<uint8_t>> sysex ownership in
  both AUv2 and AUv3 with a pooled SysExBufferPool that recycles buffer storage
  across cycles, so steady-state rendering does not allocate on the audio thread.
  (Pool growth moves the inner vector objects, not their heap storage, so
  already-handed-out payload pointers stay valid.)

Correctness:
- AUv3 UMP output timestamps were double-offset: MIDIEventList packet timestamps
  are relative to the block's AudioTimeStamp, while the legacy 3-byte block takes
  absolute sample time. Use bare per-event offsets for UMP, absolute for legacy.
- AUv3 put raw MIDI 2.0 (MT 0x4) words into a protocol-1.0 UMP list, which is
  invalid; down-convert MIDI2→MIDI1 for both delivery paths (the framework
  up-converts the list to the host's negotiated protocol), matching AUv2.
- AUv2 MIDIEventList input now honors inOffsetSampleFrame (base + packet
  timeStamp), and process() clamps all event times into [0, frames_count) so a
  host stamping packets with out-of-range times can't make plugins read past
  their buffers.
- AUv2: a note-on with velocity 0 is now treated as a note-off (MIDI running-
  status convention), matching AUv3.
- setupProcessing clamps the CLAP audio-port counts to the AU element counts, so
  a plugin that rescans its ports while deactivated can't make process() index
  past the allocation.

Shared / de-dup:
- New shared noteExpressionToMidi1() (pressure→poly/channel aftertouch,
  tuning→pitch bend) used by both AUv2 send() and the AUv3 output drain; AUv2
  output gains the channel-pressure and pitch-bend cases it was missing.

CI:
- pullreq.yml build_feature passes -DCLAP_WRAPPER_BUILD_AAX=TRUE: making AAX
  opt-in had silently dropped it from that job's coverage.

Verified: AUv2 builds -Werror (Ninja); the AUv3 appex compiles, links and
codesigns via the Xcode generator at -target arm64-apple-macos10.13.

* AAX: implement MIDI output

Wire up the MIDI-out path that was stubbed:
- register the LocalOutput node against mOutputNode (was mis-registered
  to the input field) + add the no-midi-out placeholder
- post note on/off, raw MIDI, MIDI2 (down-converted: MT 0x4 via the
  shared kernel, MT 0x2 unpacked) and SysEx (best-effort) to the node
- filter to the channel-voice messages Pro Tools routes from a plug-in
  (notes, poly/channel pressure, pitch bend, program change, CC #0)

* AAX: instantiate pure-MIDI CLAPs as Pro Tools MIDI-effect plugins

A CLAP with no audio ports produced no AAX component and couldn't load.
Wrap it as an audio-passthrough MIDI-effect, matching the SDK's
DemoMIDI_Transpose:
- inject placeholder Mono/Stereo stems when a plugin has MIDI but no
  audio ports (and stop emitting a bogus 0-channel stem)
- capture the negotiated stem's channel count at activation
- pass the AAX audio input straight through to the output (silence
  where there's no matching input) so the insert is audio-transparent

MIDIEffect category already derives from the CLAP note-effect feature.
* adding update of parameter values after load
* fixing a small issue when param_rescan is called with other flags, de-duplified value update on two places
* replaces pr #476

Thanks to olilarkin for bringing it up.
Clumps handed out a const char* into its own std::map and CopyClumpName read
through it with no lock held, while setupParameters() cleared that map on the
main thread on every param rescan. A host is free to build its parameter tree
wherever it likes -- Logic does it asynchronously, on a queue of its own, in
response to the very PropertyChanged the rescan just sent -- so for plugins
which rescan while audio is running the main thread frees the strings the host
is in the middle of reading. It faults in CopyClumpName on the host's tree
builder thread.

getClump now answers by value under a mutex, and the map is no longer emptied at
all. Clump ids outlive the rescan that minted them: a host keeps the id it read
from AudioUnitParameterInfo and asks for its name whenever it likes, so
renumbering from zero was making them wrong quite apart from the clearing making
them dangerous. addClump() dedupes, so the map converges on the module paths the
plugin reports; setupParameters() seeds it, so the ids follow parameter order
and are settled before the host is told there is anything to read.

_parametertree had the same window. GetParameterInfo CFRetains a Parameter's
CFString on the host's thread while updateInfo CFReleases and recreates it on
the main one, and the map was being searched and inserted into at once. A mutex
now spans setupParameters and the property getters. The audio thread is
deliberately not part of it -- SetParameter and the process adapter read the
tree under render and must not block -- so SetBypassEffect and onIdle copy what
they need out under the lock and act outside it, which also keeps
PropertyChanged's synchronous host listeners off it.

Two smaller faults in the same functions:

- CopyClumpName read a desired length of zero as "the empty string". The SDK
  clamps kAudioUnitParameterName_Full (-1) to zero on the way in, which is
  exactly how a host asks for the untruncated name, so every clump came back
  blank. It also returned noErr with a null CFStringRef when the conversion
  failed, and the host releases that.

- A parameter name that is not valid UTF-8 leaves Parameter::_cfstring null,
  and it was CFRetained by the caller and CFReleased in the destructor
  regardless. It falls back to Latin-1, which accepts any byte.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* adding update of parameter values after load

* fixing a small issue when param_rescan is called with other flags, de-duplified value update on two places
@defiantnerd
defiantnerd requested a review from baconpaul August 8, 2026 14:40
@baconpaul

Copy link
Copy Markdown
Collaborator

Looks great to me. Happy for this to become 0.16

Note I didn't review auv3 and have not tested auv2 midi2 / ump but reviewed rest

@defiantnerd
defiantnerd merged commit 1cca996 into main Aug 8, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants