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
2 changes: 1 addition & 1 deletion .github/workflows/pullreq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:

- name: Build project
run: |
cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCLAP_SDK_ROOT=deps/clap -DVST3_SDK_ROOT=deps/vst3sdk -DAAX_SDK_ROOT=deps/aax -DCLAP_WRAPPER_OUTPUT_NAME=testplug
cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCLAP_SDK_ROOT=deps/clap -DVST3_SDK_ROOT=deps/vst3sdk -DAAX_SDK_ROOT=deps/aax -DCLAP_WRAPPER_BUILD_AAX=TRUE -DCLAP_WRAPPER_OUTPUT_NAME=testplug
cmake --build ./build --config Debug

- name: Show Build Results
Expand Down
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
# CLAP_WRAPPER_BUILD_AUV2 Build the AUv2 wrapper (.component); requires the AudioUnitSDK
# CLAP_WRAPPER_BUILD_AUV3 Build the AUv3 app extension (.appex) plus its standalone
# host app; requires the Xcode generator (-G Xcode)
# CLAP_WRAPPER_BUILD_AAX Build the AAX wrapper; only effective where AAX is supported
# (macOS and non-ARM MSVC Windows, never Linux) and requires
# the AAX SDK
# CLAP_WRAPPER_BUILD_STANDALONE Build a simple standalone executable hosting the CLAP
# CLAP_WRAPPER_BUILD_TESTS Build the test CLAP wrappers
#
Expand Down Expand Up @@ -95,7 +98,7 @@ option(CLAP_WRAPPER_BUILD_TESTS "Build test CLAP wrappers" OFF)

project(clap-wrapper
LANGUAGES C CXX
VERSION 0.15.1
VERSION 0.16.0
DESCRIPTION "CLAP-as-X wrappers"
)
set(CLAP_WRAPPER_VERSION "${PROJECT_VERSION}" CACHE STRING "Version of the wrapper project")
Expand Down
12 changes: 10 additions & 2 deletions cmake/top_level_default.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ if (PROJECT_IS_TOP_LEVEL)
string(MAKE_C_IDENTIFIER ${CLAP_WRAPPER_OUTPUT_NAME} pluginname)


if (CLAP_WRAPPER_CAN_BUILD_AAX)
# Link the actual plugin library
# AAX is opt-in like AUv2/AUv3, and only available where the platform/toolchain
# supports it (macOS and non-ARM MSVC Windows, never Linux). Requiring both the
# opt-in and the capability avoids fetching the AAX SDK when it was not asked for.
if (CLAP_WRAPPER_BUILD_AAX)
if (CLAP_WRAPPER_CAN_BUILD_AAX)
# Link the actual plugin library
add_library(${pluginname}_as_aax MODULE)
target_add_aax_wrapper(
TARGET ${pluginname}_as_aax
OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME}"
BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}"
BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}"
)
else()
message(WARNING "clap-wrapper: CLAP_WRAPPER_BUILD_AAX was requested but AAX is not "
"supported on this platform/toolchain - skipping the AAX wrapper")
endif()
endif()

# Link the actual plugin library
Expand Down
35 changes: 23 additions & 12 deletions cmake/wrap_wclap.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,33 @@ function(target_add_wclap_configuration)
set(TCLP_OUTPUT_NAME TCLP_TARGET)
endif()

set_target_properties(${TCLP_TARGET}
PROPERTIES
OUTPUT_NAME "${TCLP_OUTPUT_NAME}"
SUFFIX ".wclap/module.wasm"
PREFIX ""
)
# Make sure directory exists
add_custom_command(TARGET ${TCLP_TARGET} PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${TCLP_TARGET}>"
)
# If a resource directory is defined, make a WCLAP bundle
if(TCLP_RESOURCE_DIRECTORY AND NOT TCLP_RESOURCE_DIRECTORY STREQUAL "")
set_target_properties(${TCLP_TARGET}
PROPERTIES
OUTPUT_NAME "${TCLP_OUTPUT_NAME}"
SUFFIX ".wclap/module.wasm"
PREFIX ""
)
# Make sure directory exists
add_custom_command(TARGET ${TCLP_TARGET} PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${TCLP_TARGET}>"
)

# Copy resource directory, if defined
if(NOT TCLP_RESOURCE_DIRECTORY STREQUAL "")
add_custom_command(TARGET ${TCLP_TARGET} PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory "${TCLP_RESOURCE_DIRECTORY}" "$<TARGET_FILE_DIR:${TCLP_TARGET}>"
)
# .tar.gz bundle of the directory
add_custom_command(TARGET ${TCLP_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E tar cz "$<TARGET_FILE_DIR:${TCLP_TARGET}>.tar.gz" "$<TARGET_FILE_DIR:${TCLP_TARGET}>"
)
else()
set_target_properties(${TCLP_TARGET}
PROPERTIES
OUTPUT_NAME "${TCLP_OUTPUT_NAME}"
SUFFIX ".wclap.wasm"
PREFIX ""
)
endif()

if (${CLAP_WRAPPER_COPY_AFTER_BUILD})
Expand Down
21 changes: 20 additions & 1 deletion src/detail/aax/audioconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,14 @@ plugin_bus_info_t getAvailableBusConfigs(Clap::Library *factory, uint32_t index)
break;
}
}
result.stemformats.push_back({f, informat, outformat});
// Only record a stem format when the plugin actually has audio ports.
// A note-only plugin (0 in / 0 out) must leave stemformats empty so the
// placeholder-stem path below can give it a Mono/Stereo passthrough
// component; otherwise it would describe a bogus 0-channel component.
if (numinputs > 0 || numoutputs > 0)
{
result.stemformats.push_back({f, informat, outformat});
}
}
}
else
Expand Down Expand Up @@ -386,6 +393,18 @@ plugin_bus_info_t getAvailableBusConfigs(Clap::Library *factory, uint32_t index)
}
}

// Pure-MIDI plugin (no audio ports) with MIDI: Pro Tools MIDI-effect
// plugins are audio components that pass audio through (see the AAX SDK's
// DemoMIDI_Transpose). Offer placeholder Mono and Stereo stems so the
// effect can be inserted on mono/stereo Instrument tracks; the wrapper
// passes the AAX audio through untouched at process time (the CLAP itself
// sees no audio ports).
if (result.stemformats.empty() && (result.has_midi_in || result.has_midi_out))
{
result.stemformats.push_back({"Mono", AAX_eStemFormat_Mono, AAX_eStemFormat_Mono});
result.stemformats.push_back({"Stereo", AAX_eStemFormat_Stereo, AAX_eStemFormat_Stereo});
}

#if (CLAP_WRAPPER_LOGLEVEL == 2)
// logging out some details
LOGDETAIL(fmt::format("the following configurations have been determined for plugin {}:",
Expand Down
189 changes: 151 additions & 38 deletions src/detail/aax/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@

#include "AAX_MIDIUtilities.h"

#include "../shared/midi_translation.h"
#include <algorithm>
#include <cmath>
#include <cstring>

namespace
{
// MIDI 1.0 channel-voice message length from the status byte: program change
// (0xC0) and channel pressure (0xD0) are 2 bytes, everything else 3.
uint32_t midi1CVLength(uint8_t status)
{
const uint8_t hi = status & 0xF0;
return (hi == 0xC0 || hi == 0xD0) ? 2 : 3;
}

// Pro Tools only routes a fixed set of channel-voice messages out of a plug-in
// (per AAX_IMIDINode::PostMIDIPacket docs): note on/off, poly & channel pressure,
// pitch bend, program change, and bank-select (controller #0). Everything else
// (other CCs, system messages, realtime) is dropped before it reaches the node.
bool proToolsAcceptsMidi1(uint8_t status, uint8_t data1)
{
switch (status & 0xF0)
{
case 0x80: // note off
case 0x90: // note on
case 0xA0: // poly key pressure
case 0xC0: // program change (no bank)
case 0xD0: // channel pressure
case 0xE0: // pitch bend
return true;
case 0xB0: // control change: PT only accepts bank select (CC #0)
return data1 == 0;
default:
return false;
}
}
} // namespace

void AAX_CALLBACK AAXWrapper_AlgorithmProcessProc(
SAAX_Wrapper_AlgorithmicContext *const inInstancesBegin[], const void *inInstancesEnd)
{
Expand Down Expand Up @@ -60,7 +98,8 @@ void AAXProcessAdapter::setupProcessing(const clap_plugin_t *plugin, double samp
const clap_plugin_audio_ports *ext_audio,
Clap::IAutomation *automation,
std::vector<clap_id> &gesturedparameters,
ParamChangeQueue &inqueue, uint32_t midiportid, bool preferMIDI)
ParamChangeQueue &inqueue, uint32_t midiportid, bool preferMIDI,
uint32_t placeholderInChannels, uint32_t placeholderOutChannels)
{
_plugin = plugin;
_ext_param = ext_param;
Expand All @@ -71,6 +110,9 @@ void AAXProcessAdapter::setupProcessing(const clap_plugin_t *plugin, double samp
_midi_first_portid = midiportid;
_midi_prefer_mididialect = preferMIDI;

_placeholderInChannels = placeholderInChannels;
_placeholderOutChannels = placeholderOutChannels;

// other needed references like buffers, MIDINodes etc. are passed
// via the SAAX_Wrapper_AlgorithmicContext to the process function
// prepare the proc member
Expand Down Expand Up @@ -157,6 +199,11 @@ void AAXProcessAdapter::process(SAAX_Wrapper_AlgorithmicContext *context)
// transport
auto aax_transport = context->mTransportNode->GetTransport();

// capture the MIDI output node for this cycle. enqueueOutputEvent() posts to
// it directly while the plugin is inside _plugin->process() (the CLAP event
// buffers are only guaranteed valid during that call). Reset afterwards.
_outputNode = context->mOutputNode;

// this clears the vectors (which do not resize to smaller)
this->_events.clear();
this->_eventindices.clear();
Expand Down Expand Up @@ -344,12 +391,28 @@ void AAXProcessAdapter::process(SAAX_Wrapper_AlgorithmicContext *context)
break;
}

// no support for outgoing MIDI at this time
//if (context->mOutputNode)
//{
// auto midiOutputStream = context->mOutputNode->GetNodeBuffer();
// // do MIDI out
//}
// Placeholder-stem passthrough: for a pure-MIDI CLAP the component still
// carries an AAX audio bus but the CLAP has no audio ports, so it never wrote
// the output. Copy the AAX input straight to the output (silence if there is
// no matching input channel) so the MIDI-effect insert is audio-transparent,
// matching the AAX SDK's DemoMIDI_Transpose.
if (_placeholderOutChannels > 0)
{
const int32_t numSamples = *(context->mNumSamples);
for (uint32_t ch = 0; ch < _placeholderOutChannels; ++ch)
{
float *out = context->mAudioOutputs ? context->mAudioOutputs[ch] : nullptr;
if (!out) continue;
if (ch < _placeholderInChannels && context->mAudioInputs && context->mAudioInputs[ch])
memcpy(out, context->mAudioInputs[ch], sizeof(float) * (size_t)numSamples);
else
memset(out, 0, sizeof(float) * (size_t)numSamples);
}
}

// Outgoing MIDI was already posted to _outputNode from enqueueOutputEvent()
// during _plugin->process(). Drop the node so it can never be used stale.
_outputNode = nullptr;
}

void AAXProcessAdapter::flush()
Expand Down Expand Up @@ -389,41 +452,18 @@ bool AAXProcessAdapter::enqueueOutputEvent(const clap_event_header_t *event)
{
case CLAP_EVENT_NOTE_ON:
{
// auto nevt = reinterpret_cast<const clap_event_note*>(event);

/*
Steinberg::Vst::Event oe{};
oe.type = Steinberg::Vst::Event::kNoteOnEvent;
oe.noteOn.channel = nevt->channel;
oe.noteOn.pitch = nevt->key;
oe.noteOn.velocity = nevt->velocity;
oe.noteOn.length = 0;
oe.noteOn.tuning = 0.0f;
oe.noteOn.noteId = nevt->note_id;
oe.busIndex = 0; // FIXME - multi-out midi still needs work
oe.sampleOffset = nevt->header.time;

if (_vstdata && _vstdata->outputEvents) _vstdata->outputEvents->addEvent(oe);
*/
auto nevt = reinterpret_cast<const clap_event_note *>(event);
uint8_t bytes[3] = {(uint8_t)(0x90 | (nevt->channel & 0x0F)), (uint8_t)(nevt->key & 0x7F),
(uint8_t)(std::lround(nevt->velocity * 127.0) & 0x7F)};
postMIDI1(nevt->header.time, bytes, 3);
}
return true;
case CLAP_EVENT_NOTE_OFF:
{
// auto nevt = reinterpret_cast<const clap_event_note*>(event);
/*
Steinberg::Vst::Event oe{};
oe.type = Steinberg::Vst::Event::kNoteOffEvent;
oe.noteOff.channel = nevt->channel;
oe.noteOff.pitch = nevt->key;
oe.noteOff.velocity = nevt->velocity;
oe.noteOn.length = 0;
oe.noteOff.tuning = 0.0f;
oe.noteOff.noteId = nevt->note_id;
oe.busIndex = 0; // FIXME - multi-out midi still needs work
oe.sampleOffset = nevt->header.time;

if (_vstdata && _vstdata->outputEvents) _vstdata->outputEvents->addEvent(oe);
*/
auto nevt = reinterpret_cast<const clap_event_note *>(event);
uint8_t bytes[3] = {(uint8_t)(0x80 | (nevt->channel & 0x0F)), (uint8_t)(nevt->key & 0x7F),
(uint8_t)(std::lround(nevt->velocity * 127.0) & 0x7F)};
postMIDI1(nevt->header.time, bytes, 3);
}
return true;
case CLAP_EVENT_NOTE_END:
Expand Down Expand Up @@ -462,8 +502,42 @@ bool AAXProcessAdapter::enqueueOutputEvent(const clap_event_header_t *event)
break;

case CLAP_EVENT_MIDI:
{
auto mevt = reinterpret_cast<const clap_event_midi *>(event);
postMIDI1(mevt->header.time, mevt->data, midi1CVLength(mevt->data[0]));
}
return true;
break;
case CLAP_EVENT_MIDI_SYSEX:
{
auto sevt = reinterpret_cast<const clap_event_midi_sysex *>(event);
postSysEx(sevt->header.time, sevt->buffer, sevt->size);
}
return true;
break;
case CLAP_EVENT_MIDI2:
{
// AAX is a MIDI 1.0 wire, so down-convert. Only channel-voice UMP
// messages have a MIDI 1.0 equivalent: MT 0x4 (MIDI 2.0 CV) via the
// shared kernel, MT 0x2 (a MIDI 1.0 CV already wrapped in a UMP) by
// unpacking the three bytes. SysEx7 (MT 0x3) and utility messages are
// dropped (Pro Tools would not route them anyway).
auto m2evt = reinterpret_cast<const clap_event_midi2 *>(event);
const uint32_t messageType = (m2evt->data[0] >> 28) & 0xFu;
if (messageType == 0x4u)
{
uint8_t out[3];
int n = ClapWrapper::detail::shared::midi2ChannelVoiceToMidi1(m2evt->data, out);
if (n > 0) postMIDI1(m2evt->header.time, out, (uint32_t)n);
}
else if (messageType == 0x2u)
{
const uint8_t status = (uint8_t)((m2evt->data[0] >> 16) & 0xFFu);
uint8_t out[3] = {status, (uint8_t)((m2evt->data[0] >> 8) & 0xFFu),
(uint8_t)(m2evt->data[0] & 0xFFu)};
postMIDI1(m2evt->header.time, out, midi1CVLength(status));
}
}
return true;
break;
default:
Expand Down Expand Up @@ -501,6 +575,45 @@ void AAXProcessAdapter::removeFromActiveNotes(const clap_event_note *note)
}
}

void AAXProcessAdapter::postMIDI1(uint32_t timestamp, const uint8_t *bytes, uint32_t length)
{
// No output node when the plugin declares no MIDI out (the descriptor
// installs a private-data placeholder instead) or outside a process() call.
if (!_outputNode || length == 0) return;
if (length > 4) length = 4; // AAX_CMidiPacket carries at most 4 bytes

// Drop anything Pro Tools won't route out of a plug-in.
if (!proToolsAcceptsMidi1(bytes[0], length > 1 ? bytes[1] : 0)) return;

AAX_CMidiPacket packet;
packet.mTimestamp = timestamp;
packet.mLength = length;
packet.mIsImmediate = false;
for (uint32_t i = 0; i < 4; ++i) packet.mData[i] = (i < length) ? bytes[i] : 0;

_outputNode->PostMIDIPacket(&packet);
}

void AAXProcessAdapter::postSysEx(uint32_t timestamp, const uint8_t *data, uint32_t size)
{
// AAX carries SysEx as a series of AAX_CMidiPackets of up to 4 bytes each,
// sharing one timestamp; the host reassembles them. The 0xF0/0xF7 framing is
// kept intact. Note: Pro Tools does not list SysEx among the MIDI messages it
// routes out of a plug-in, so this is best-effort for hosts that do.
if (!_outputNode || !data || size == 0) return;

for (uint32_t offset = 0; offset < size; offset += 4)
{
const uint32_t chunk = std::min<uint32_t>(4, size - offset);
AAX_CMidiPacket packet;
packet.mTimestamp = timestamp;
packet.mLength = chunk;
packet.mIsImmediate = false;
for (uint32_t i = 0; i < 4; ++i) packet.mData[i] = (i < chunk) ? data[offset + i] : 0;
_outputNode->PostMIDIPacket(&packet);
}
}

uint32_t AAXProcessAdapter::input_events_size(const struct clap_input_events *list)
{
auto self = static_cast<AAXProcessAdapter *>(list->ctx);
Expand Down
Loading
Loading