Version 0.16.0 - #504
Merged
Merged
Conversation
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
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.16.0: merge next into main
AudioUnitParameterInfo.unit instead of corrupting the flags bitfield, and
kAudioUnitProperty_BypassEffect now drives and follows the plugin's CLAP
bypass parameter (fixing two bugs in parameters and bypass on AUv2 #489).
the current thread's run loop, so it fires reliably (macOS Timer attach to main loop #494).
.wclap/ directory bundle plus its distributable .tar.gz are only produced
when a resource directory is defined (Only make WCLAP bundle (
.tar.gz) if a resource directory is defined #495).negotiation and SysEx support, reaching MIDI parity with the VST3 wrapper
(AUv2: full MIDI 1.0 + MIDI 2.0 (UMP) in/out, plus minor fixes #493).
count, fixing mono aufx configurations in auval and Logic and plugging a
bus-name string leak (Fix AUv2 channel count handling, memory leak #496).
(placeholder bus, SupportedNumChannels, ValidFormat) so they pass auval
(AUv2: present a silent stereo in/out facade for effects with no audio ports #497).
longer feeds uninitialized memory to buses during validation (Create silence buffers as empty buffers #498).
the host reads an up-to-date parameter cache, fixing VST3: state not restored in Ableton Live — setComponentState is not implemented #475 (adding update of parameter values after load #499).
parameter rescan — clump names are returned by value under a mutex, clump
ids stay stable across rescans, and CopyClumpName/non-UTF-8 name handling
were corrected (AUv2: fix a use-after-free reading parameter clump names #500).
so no resize or destroy callbacks reach a GUI that was never created, and
creation can be retried (AUv3: clear created state when GUI creation fails #501).