Skip to content

Commit

Permalink
Macros as Plugin Edge Parameters working (#1107)
Browse files Browse the repository at this point in the history
- Macros appear as CLAP/VST3/AU parameters
- Lots had to happen to make this work including parameter id math,
  messages across the queues, implementing the params in clap,
  and other bits and bobs involving the wrapper. So this is a big
  diff.
- Biggest differences include engine output audio event queue optionally
  and robust clap params.
- Some bugs known still include
    - engine -> ui doesn't throttle at all on automation. OK?
    - vst3 and auv2 rename doesn't reset param name. CLAP does.
  • Loading branch information
baconpaul authored Aug 14, 2024
1 parent 2c54f96 commit dfc5977
Show file tree
Hide file tree
Showing 17 changed files with 453 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ struct SCXTApplicationWindow : juce::DocumentWindow, juce::Button::Listener
optionsButton("Options")
{
engine = std::make_unique<scxt::engine::Engine>();
engine->runningEnvironment = "Temporary SCXT Standalone";

editor = std::make_unique<scxt::ui::SCXTEditor>(
*(engine->getMessageController()), *(engine->defaults), *(engine->getSampleManager()),
Expand Down
200 changes: 199 additions & 1 deletion clients/clap-first/scxt-plugin/scxt-plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* https://github.com/surge-synthesizer/shortcircuit-xt
*/

#include <cassert>

#include "scxt-plugin.h"
#include "version.h"
#include "components/SCXTEditor.h"
Expand Down Expand Up @@ -59,7 +61,18 @@ const clap_plugin *makeSCXTPlugin(const clap_host *host)
SCXTPlugin::SCXTPlugin(const clap_host *h) : plugHelper_t(getDescription(), h)
{
engine = std::make_unique<scxt::engine::Engine>();
engine->runningEnvironment = "Clap-First Plugin plus Wrapper";
engine->getMessageController()->passWrapperEventsToWrapperQueue = true;
engine->runningEnvironment =
"Clap-First " + std::string(h->name) + " " + std::string(h->version);
engine->getMessageController()->requestHostCallback = [this, h](uint64_t flag) {
if (h)
{
SCLOG("Request Host CallbacK");
// FIXME - atomics better here
this->nextMainThreadAction |= flag;
h->request_callback(h);
}
};

clapJuceShim = std::make_unique<sst::clap_juce_shim::ClapJuceShim>(this);
clapJuceShim->setResizable(true);
Expand Down Expand Up @@ -92,6 +105,7 @@ std::unique_ptr<juce::Component> SCXTPlugin::createEditor()
ed->setSize(scxt::ui::SCXTEditor::edWidth, scxt::ui::SCXTEditor::edHeight);
return ed;
}

bool SCXTPlugin::stateSave(const clap_ostream *ostream) noexcept
{
engine->getSampleManager()->purgeUnreferencedSamples();
Expand All @@ -117,6 +131,7 @@ bool SCXTPlugin::stateSave(const clap_ostream *ostream) noexcept
}
return true;
}

bool SCXTPlugin::stateLoad(const clap_istream *istream) noexcept
{
static constexpr uint32_t initSize = 1 << 16, chunkSize = 1 << 8;
Expand All @@ -141,8 +156,13 @@ bool SCXTPlugin::stateLoad(const clap_istream *istream) noexcept

auto xml = std::string(buffer.data());

SCLOG("About to load state with size " << xml.size());
scxt::messaging::client::clientSendToSerialization(
scxt::messaging::client::UnstreamIntoEngine{xml}, *engine->getMessageController());
scxt::messaging::client::clientSendToSerialization(
scxt::messaging::client::RequestHostCallback{(uint64_t)RESCAN_PARAM_IVT},
*engine->getMessageController());

return true;
}

Expand All @@ -153,6 +173,7 @@ uint32_t SCXTPlugin::audioPortsCount(bool isInput) const noexcept
else
return numPluginOutputs;
}

bool SCXTPlugin::audioPortsInfo(uint32_t index, bool isInput,
clap_audio_port_info *info) const noexcept
{
Expand Down Expand Up @@ -200,8 +221,12 @@ bool SCXTPlugin::notePortsInfo(uint32_t index, bool isInput,
}
return true;
}

clap_process_status SCXTPlugin::process(const clap_process *process) noexcept
{
#if BUILD_IS_DEBUG
engine->getMessageController()->threadingChecker.registerAsAudioThread();
#endif
float **out = process->audio_outputs[0].data32;
auto chans = process->audio_outputs->channel_count;
if (chans != 2)
Expand Down Expand Up @@ -248,6 +273,8 @@ clap_process_status SCXTPlugin::process(const clap_process *process) noexcept
auto ev = process->in_events;
auto sz = ev->size(ev);

auto ov = process->out_events;

const clap_event_header_t *nextEvent{nullptr};
uint32_t nextEventIndex{0};
if (sz != 0)
Expand All @@ -273,6 +300,66 @@ clap_process_status SCXTPlugin::process(const clap_process *process) noexcept
engine->processAudio();
engine->transport.timeInBeats += (double)scxt::blockSize * engine->transport.tempo *
engine->getSampleRateInv() / 60.f;

bool tryToDrain{true};
while (tryToDrain &&
!engine->getMessageController()->engineToPluginWrapperQueue.empty())
{
auto msgopt = engine->getMessageController()->engineToPluginWrapperQueue.pop();
if (!msgopt.has_value())
{
tryToDrain = false;
break;
}

switch (msgopt->id)
{
case messaging::audio::s2a_param_beginendedit:
{
auto begin = msgopt->payload.mix.u[0];
auto parm = msgopt->payload.mix.u[1];
auto val = msgopt->payload.mix.f[0];

auto evt = clap_event_param_gesture();
evt.header.size = sizeof(clap_event_param_gesture);
evt.header.type =
(begin ? CLAP_EVENT_PARAM_GESTURE_BEGIN : CLAP_EVENT_PARAM_GESTURE_END);
evt.header.time = s;
evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
evt.header.flags = 0;
evt.param_id = parm;

ov->try_push(ov, &evt.header);
}
break;
case messaging::audio::s2a_param_set_value:
{
auto parm = msgopt->payload.mix.u[0];
auto val = msgopt->payload.mix.f[0];

auto evt = clap_event_param_value();
evt.header.size = sizeof(clap_event_param_value);
evt.header.type = (uint16_t)CLAP_EVENT_PARAM_VALUE;
evt.header.time = 0; // for now
evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
evt.header.flags = 0;
evt.param_id = parm;
evt.value = val;

ov->try_push(ov, &evt.header);
}
break;
case messaging::audio::s2a_param_refresh:
{
nextMainThreadAction |= RESCAN_PARAM_IVT;
_host.requestCallback();
}
break;
default:
SCLOG("Unexpected message " << msgopt->id);
break;
}
}
}

// TODO: this can be way more efficient and block wise and stuff
Expand Down Expand Up @@ -348,6 +435,12 @@ bool SCXTPlugin::handleEvent(const clap_event_header_t *nextEvent)
engine->voiceManager.processNoteOffEvent(nevt->port_index, nevt->channel, nevt->key,
nevt->note_id, nevt->velocity);
}

case CLAP_EVENT_PARAM_VALUE:
{
auto pevt = reinterpret_cast<const clap_event_param_value *>(nextEvent);
handleParamValueEvent(pevt);
}
break;
}
}
Expand All @@ -361,4 +454,109 @@ bool SCXTPlugin::activate(double sampleRate, uint32_t minFrameCount,
return true;
}

/*
* Parameter support
*/
uint32_t SCXTPlugin::paramsCount() const noexcept { return scxt::numParts * scxt::macrosPerPart; }

using mac = engine::Macro;

bool SCXTPlugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noexcept
{
static constexpr int firstMacroIndex{0};
static constexpr int lastMacroIndex{scxt::numParts * scxt::macrosPerPart};
if (paramIndex >= firstMacroIndex && paramIndex < lastMacroIndex)
{
int p0 = paramIndex - firstMacroIndex;
int index = p0 % scxt::macrosPerPart;
int part = p0 / scxt::macrosPerPart;

info->id = mac::partIndexToMacroID(part, index);
assert(mac::macroIDToPart(info->id) == part);
assert(mac::macroIDToIndex(info->id) == index);
info->flags = CLAP_PARAM_IS_AUTOMATABLE;
info->cookie = nullptr; // super easy to reverse engineer
const auto &macro = engine->getPatch()->getPart(part)->macros[index];
strncpy(info->name, macro.pluginParameterNameFor(part, index).c_str(), CLAP_NAME_SIZE);
std::string moduleName = "Part " + std::to_string(part + 1) + " / Macros";
strncpy(info->module, moduleName.c_str(), CLAP_NAME_SIZE);
info->min_value = 0;
info->max_value = 1;
info->default_value = (macro.isBipolar ? 0.5 : 0);
return true;
}
return false;
}

bool SCXTPlugin::paramsValue(clap_id paramId, double *value) noexcept
{
if (mac::isMacroID(paramId))
{
*value = macroFor(paramId).getValue01();

return true;
}
return false;
}

bool SCXTPlugin::paramsTextToValue(clap_id paramId, const char *display, double *value) noexcept
{
if (mac::isMacroID(paramId))
{
*value = macroFor(paramId).value01FromString(display);
return true;
}
return false;
}
bool SCXTPlugin::paramsValueToText(clap_id paramId, double value, char *display,
uint32_t size) noexcept
{
if (mac::isMacroID(paramId))
{
auto fs = macroFor(paramId).value01ToString(value);

strncpy(display, fs.c_str(), size);
return true;
}
return false;
}
void SCXTPlugin::paramsFlush(const clap_input_events *ev, const clap_output_events *out) noexcept
{
auto sz = ev->size(ev);
for (int ei = 0; ei < sz; ++ei)
{
auto nextEvent = ev->get(ev, ei);
if (nextEvent->space_id == CLAP_CORE_EVENT_SPACE_ID &&
nextEvent->type == CLAP_EVENT_PARAM_VALUE)
{
auto pevt = reinterpret_cast<const clap_event_param_value *>(nextEvent);
handleParamValueEvent(pevt);
}
}
}

void SCXTPlugin::handleParamValueEvent(const clap_event_param_value *pev)
{
if (mac::isMacroID(pev->param_id))
{
engine->setMacro01ValueFromPlugin(mac::macroIDToPart(pev->param_id),
mac::macroIDToIndex(pev->param_id), pev->value);
}
}
void SCXTPlugin::onMainThread() noexcept
{
// Fixme - better atomics
uint64_t a = nextMainThreadAction;
nextMainThreadAction = 0;
if (a & RESCAN_PARAM_IVT)
{
if (_host.canUseParams())
{
SCLOG("Rescanning params");
_host.paramsRescan(CLAP_PARAM_RESCAN_INFO | CLAP_PARAM_RESCAN_VALUES |
CLAP_PARAM_RESCAN_TEXT);
}
}
}

} // namespace scxt::clap_first::scxt_plugin
31 changes: 31 additions & 0 deletions clients/clap-first/scxt-plugin/scxt-plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include <memory>
#include <random>
#include <tuple>
#include <cstdint>

#include "sst/clap_juce_shim/clap_juce_shim.h"

Expand All @@ -63,6 +64,16 @@ struct SCXTPlugin : public plugHelper_t, sst::clap_juce_shim::EditorProvider
bool activate(double sampleRate, uint32_t minFrameCount,
uint32_t maxFrameCount) noexcept override;

/*
* What can we do on main thread
*/
enum MainThreadActions : uint64_t
{
RESCAN_PARAM_IVT = 1 << 0,
};
std::atomic<uint64_t> nextMainThreadAction;
void onMainThread() noexcept override;

bool implementsAudioPorts() const noexcept override { return true; }
uint32_t audioPortsCount(bool isInput) const noexcept override;
bool audioPortsInfo(uint32_t index, bool isInput,
Expand All @@ -80,6 +91,26 @@ struct SCXTPlugin : public plugHelper_t, sst::clap_juce_shim::EditorProvider
bool stateSave(const clap_ostream *stream) noexcept override;
bool stateLoad(const clap_istream *stream) noexcept override;

bool implementsParams() const noexcept override { return true; }
uint32_t paramsCount() const noexcept override;
bool paramsInfo(uint32_t paramIndex, clap_param_info *info) const noexcept override;
bool paramsValue(clap_id paramId, double *value) noexcept override;
bool paramsValueToText(clap_id paramId, double value, char *display,
uint32_t size) noexcept override;
bool paramsTextToValue(clap_id paramId, const char *display, double *value) noexcept override;
void paramsFlush(const clap_input_events *in, const clap_output_events *out) noexcept override;

const scxt::engine::Macro &macroFor(uint32_t paramId) const
{
auto part = engine::Macro::macroIDToPart(paramId);
auto idx = engine::Macro::macroIDToIndex(paramId);
assert(part >= 0 && part < scxt::numParts);
assert(idx >= 0 && idx < scxt::macrosPerPart);
return engine->getPatch()->getPart(part)->macros[idx];
}

void handleParamValueEvent(const clap_event_param_value *);

public:
bool implementsGui() const noexcept override { return clapJuceShim != nullptr; }
std::unique_ptr<sst::clap_juce_shim::ClapJuceShim> clapJuceShim;
Expand Down
2 changes: 1 addition & 1 deletion libs/clap/clap-wrapper
6 changes: 4 additions & 2 deletions src-ui/components/SCXTEditorResponseHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,10 @@ void SCXTEditor::onMacroFullState(const scxt::messaging::client::macroFullState_
multiScreen->sample->macroDataChanged(part, index);
}

void SCXTEditor::onMacroValue(const scxt::messaging::client::macroValue_t &)
void SCXTEditor::onMacroValue(const scxt::messaging::client::macroValue_t &s)
{
SCLOG_WFUNC("Implement");
const auto &[part, index, value] = s;
macroCache[part][index].value = value;
multiScreen->sample->repaint();
}
} // namespace scxt::ui
12 changes: 3 additions & 9 deletions src-ui/components/multi/ModPane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,11 @@ template <typename GZTrait> struct ModRow : juce::Component, HasEditor
curve->setLabel("-");

auto makeSourceName = [](auto &si, auto &sn) {
// This is the second location where we are assuming default macro name
// as mentioned in part.h
auto nm = sn.second;

if (si.gid == 'zmac' || si.gid == 'gmac')
{
auto defname = "Macro " + std::to_string(si.index + 1);
if (nm != defname)
if (nm != scxt::engine::Macro::defaultNameFor(si.index))
{
nm = "M" + std::to_string(si.index + 1) + ": " + nm;
}
Expand Down Expand Up @@ -456,12 +453,9 @@ template <typename GZTrait> struct ModRow : juce::Component, HasEditor
auto nm = sn.second;
if (si.gid == 'gmac' || si.gid == 'zmac')
{
// This is where we are assuming default macro name
// from part.h
auto defName = "Macro " + std::to_string(si.index + 1);
if (nm != defName)
if (nm != scxt::engine::Macro::defaultNameFor(si.index))
{
nm = defName + " (" + nm + ")";
nm = scxt::engine::Macro::defaultNameFor(si.index) + " (" + nm + ")";
}
}
sub.addItem(nm, true, selected, mkCallback(si));
Expand Down
Loading

0 comments on commit dfc5977

Please sign in to comment.