#include "clap/clap.h" #include #include #include #include #include #include #include "../param-info.h" #include "../storage/stfx-storage.h" #include "../ui/web-ui.h" namespace stfx { namespace clap { // A CLAP plugin made from an STFX template template class EffectSTFX> struct Plugin; // A helper to make a CLAP plugin factory from STFX templates struct Plugins { template class EffectSTFX, class ...Args> size_t add(clap_plugin_descriptor desc, std::initializer_list features, Args ...args) { size_t index = featureLists.size(); featureLists.emplace_back(features); featureLists[index].push_back(nullptr); desc.features = featureLists[index].data(); descriptors.push_back(desc); creates.push_back([=](const clap_host *host){ return new Plugin(*this, &descriptors[index], host, args...); }); return index; } bool clap_init(const char *path) { modulePath = path; return true; } void clap_deinit() {} const void * clap_get_factory(const char *id) { if (!std::strcmp(id, CLAP_PLUGIN_FACTORY_ID)) { // static variables like this are thread-safe (since C++11) // https://en.cppreference.com/w/cpp/language/storage_duration.html#Static_block_variables static PluginFactory factory{*this}; return &factory; } return nullptr; } std::string modulePath; private: std::vector> featureLists; std::vector descriptors; std::vector> creates; struct PluginFactory : public clap_plugin_factory { Plugins &plugins; PluginFactory(Plugins &plugins) : plugins(plugins) { get_plugin_count = static_get_plugin_count; get_plugin_descriptor = static_get_plugin_descriptor; create_plugin = static_create_plugin; } static uint32_t static_get_plugin_count(const clap_plugin_factory *factory) { const auto &plugins = ((PluginFactory *)factory)->plugins; return uint32_t(plugins.creates.size()); } static const clap_plugin_descriptor * static_get_plugin_descriptor(const clap_plugin_factory *factory, uint32_t index) { const auto &plugins = ((PluginFactory *)factory)->plugins; if (index >= plugins.descriptors.size()) return nullptr; return &plugins.descriptors[index]; } static const clap_plugin * static_create_plugin(const clap_plugin_factory *factory, const clap_host *host, const char *pluginId) { const auto &plugins = ((PluginFactory *)factory)->plugins; for (size_t index = 0; index < plugins.descriptors.size(); ++index) { auto &desc = plugins.descriptors[index]; if (!std::strcmp(pluginId, desc.id)) { return plugins.creates[index](host); } } return nullptr; } }; }; struct Crc32 { void add(uint8_t byte) { uint32_t val = (crc^byte)&0xFF; for (int i = 0; i < 8; ++i) { val = (val&1) ? (val>>1)^0xEDB88320 : (val>>1); } crc = val^(crc>>8); } Crc32 & addString(const char *str, bool includeNull=true) { while (*str) { add(uint8_t(*str)); ++str; } if (includeNull) add(0); return *this; } Crc32 copy() { return {*this}; } uint32_t done() const { return crc^0xFFFFFFFFu; } private: uint32_t crc = 0xFFFFFFFFu; }; // Just include the proposed draft structs here static constexpr const char *CLAP_EXT_WEBVIEW1 = "clap.webview/1"; struct clap_plugin_webview1 { bool(CLAP_ABI *provide_starting_uri)(const clap_plugin_t *plugin, char *out_buffer, uint32_t out_buffer_capacity); bool(CLAP_ABI *receive)(const clap_plugin_t *plugin, const void *buffer, uint32_t size); }; struct clap_host_webview1 { bool(CLAP_ABI *is_open)(const clap_host_t *host); bool(CLAP_ABI *send)(const clap_host_t *host, const void *buffer, uint32_t size); }; template class EffectSTFX> struct Plugin : public clap_plugin { const Plugins &plugins; const clap_host *host; const clap_host_params *hostParams = nullptr; const clap_host_webview1 *hostWebview1 = nullptr; using Effect = stfx::web::WebUILibraryEffect; Effect effect; template Plugin(const Plugins &plugins, const clap_plugin_descriptor *desc, const clap_host *host, Args ...args) : plugins(plugins), host(host), effect(args...) { this->desc = desc; this->plugin_data = nullptr; this->init = plugin_init; this->destroy = plugin_destroy; this->activate = plugin_activate; this->deactivate = plugin_deactivate; this->start_processing = plugin_start_processing; this->stop_processing = plugin_stop_processing; this->reset = plugin_reset; this->process = plugin_process; this->get_extension = plugin_get_extension; this->on_main_thread = plugin_on_main_thread; scanParams(); } // plugin state bool isConfigured = false; // for library STFX, all inputs/outputs (main and aux) get concatenated together std::vector inputBuffers; std::vector outputBuffers; void processEvent(const clap_event_header *header) { if (header->space_id != CLAP_CORE_EVENT_SPACE_ID) return; // only core events supported atm if (header->type == CLAP_EVENT_PARAM_VALUE) { auto *event = (clap_event_param_value *)header; auto *param = (Param *)event->cookie; if (!param) { auto paramId = event->param_id; for (auto &p : params) { if (p.id == paramId) { param = &p; break; } } if (!param) return; // invalid parameter } else if (param->id != event->param_id) { return; // inconsistent ID / cookie } if (param->rangeParam) { *param->rangeParam = param->rangeInfo->fromUnit(event->value); } else { *param->steppedParam = int(std::round(event->value)); } } else { LOG_EXPR(header->size); LOG_EXPR(header->time); LOG_EXPR(header->space_id); LOG_EXPR(header->type); LOG_EXPR(header->flags); } } // CLAP plugin methods static bool plugin_init(const clap_plugin *obj) { auto &plugin = *(Plugin *)obj; #define STFX_GET_EXT(field, extId) \ plugin.field = (decltype(plugin.field))plugin.host->get_extension(plugin.host, extId); STFX_GET_EXT(hostParams, CLAP_EXT_PARAMS) STFX_GET_EXT(hostWebview1, CLAP_EXT_WEBVIEW1) #undef STFX_GET_EXT return true; } static void plugin_destroy(const clap_plugin *obj) { delete (Plugin *)obj; } static bool plugin_activate(const clap_plugin *obj, double sampleRate, uint32_t minFrames, uint32_t maxFrames) { auto &plugin = *(Plugin *)obj; auto &config = plugin.effect.config; if (!plugin.isConfigured || sampleRate != plugin.effect.config.sampleRate) { auto prevConfig = config; plugin.isConfigured = plugin.effect.configure(); if (!plugin.isConfigured) { // Can't change config (e.g. sample-rate/ports/etc.) here, return to previous config = prevConfig; } } size_t inputChannels = config.inputChannels; for (auto &a : config.auxInputs) inputChannels += a; plugin.inputBuffers.reserve(inputChannels); size_t outputChannels = config.outputChannels; for (auto &a : config.auxOutputs) a += outputChannels; plugin.outputBuffers.reserve(outputChannels); return plugin.isConfigured; } static void plugin_deactivate(const clap_plugin *obj) {} static bool plugin_start_processing(const clap_plugin *obj) { auto &plugin = *(Plugin *)obj; return plugin.isConfigured; } static void plugin_stop_processing(const clap_plugin *obj) {} static void plugin_reset(const clap_plugin *obj) { auto &plugin = *(Plugin *)obj; if (plugin.isConfigured) plugin.effect.reset(); } static const void * plugin_get_extension(const clap_plugin *obj, const char *extId) { if (!std::strcmp(extId, CLAP_EXT_PARAMS)) { static struct clap_plugin_params ext{ params_count, params_get_info, params_get_value, params_value_to_text, params_text_to_value, params_flush }; return &ext; } else if (!std::strcmp(extId, CLAP_EXT_AUDIO_PORTS)) { static struct clap_plugin_audio_ports ext{ audio_ports_count, audio_ports_get }; return &ext; } else if (!std::strcmp(extId, CLAP_EXT_STATE)) { static struct clap_plugin_state ext{ state_save, state_load }; return &ext; } else if (!std::strcmp(extId, CLAP_EXT_WEBVIEW1)) { static struct clap_plugin_webview1 ext{ webview1_provide_starting_uri, webview1_receive }; return &ext; } return nullptr; } static clap_process_status plugin_process(const clap_plugin *obj, const clap_process *process) { auto &plugin = *(Plugin *)obj; auto &inputBuffers = plugin.inputBuffers; for (uint32_t i = 0; i < process->audio_inputs_count; ++i) { auto &buffer = process->audio_inputs[i]; for (uint32_t c = 0; c < buffer.channel_count; ++c) { inputBuffers.push_back(buffer.data32[c]); } } auto &outputBuffers = plugin.outputBuffers; for (uint32_t i = 0; i < process->audio_outputs_count; ++i) { auto &buffer = process->audio_outputs[i]; for (uint32_t c = 0; c < buffer.channel_count; ++c) { outputBuffers.push_back(buffer.data32[c]); } } size_t length = process->frames_count; auto inputEventCount = process->in_events->size(process->in_events); size_t offset = 0, nextEventIndex = 0; while (offset < length) { size_t nextEventOffset = length; const clap_event_header *event = nullptr; if (nextEventIndex < inputEventCount) { event = process->in_events->get(process->in_events, nextEventIndex); nextEventOffset = std::max(offset, event->time); } // process up until the next event (or end) if (nextEventOffset > offset) { auto delta = nextEventOffset - offset; plugin.effect.process(inputBuffers.data(), outputBuffers.data(), delta); offset = nextEventOffset; for (auto &p : inputBuffers) p += delta; for (auto &p : outputBuffers) p += delta; } if (event) { plugin.processEvent(event); ++nextEventIndex; } } inputBuffers.resize(0); outputBuffers.resize(0); if (plugin.effect.hasPendingWebMessage()) { plugin.host->request_callback(plugin.host); } plugin.sendParamEvents(process->out_events); return CLAP_PROCESS_CONTINUE; } static void plugin_on_main_thread(const clap_plugin *obj) { auto &plugin = *(Plugin *)obj; plugin.sendWebMessages(); } // parameters struct Param : public clap_param_info { typename Effect::ParamRange *rangeParam = nullptr; std::optional rangeInfo; typename Effect::ParamStepped *steppedParam = nullptr; std::optional steppedInfo; std::atomic_flag hostValueSent = ATOMIC_FLAG_INIT; std::atomic_flag hostStartGestureSent = ATOMIC_FLAG_INIT; std::atomic_flag hostStopGestureSent = ATOMIC_FLAG_INIT; Param() {} Param(Param &&other) : clap_param_info(other), rangeParam(other.rangeParam), rangeInfo(std::move(other.rangeInfo)), steppedParam(other.steppedParam), steppedInfo(std::move(other.steppedInfo)) {} void setClapInfo() { flags |= CLAP_PARAM_IS_AUTOMATABLE; cookie = this; if (rangeParam) { std::strncpy(name, rangeInfo->name.c_str(), CLAP_NAME_SIZE); // STFX range params are mapped to [0, 1] so we can give them a nonlinear shape min_value = 0; max_value = 1; default_value = rangeInfo->toUnit(rangeInfo->defaultValue); } else { std::strncpy(name, steppedInfo->name.c_str(), CLAP_NAME_SIZE); flags |= CLAP_PARAM_IS_STEPPED; min_value = steppedInfo->low; max_value = steppedInfo->high; default_value = steppedInfo->defaultValue; } hostValueSent.test_and_set(); hostStartGestureSent.test_and_set(); hostStopGestureSent.test_and_set(); } }; std::vector params; struct ParamScanner : public storage::STFXStorageScanner { std::vector ¶ms; Crc32 crc; ParamScanner(std::vector ¶ms) : params(params) {} template RangeParamInfo & range(const char *key, PR ¶m) { param.context.index = params.size(); // so we can find it in the listeners below params.emplace_back(); auto &entry = params.back(); entry.id = crc.copy().addString(key, true).done(); entry.rangeParam = ¶m; return entry.rangeInfo.emplace(param); } template SteppedParamInfo & stepped(const char *key, PS ¶m) { param.context.index = params.size(); // so we can find it in the listeners below params.emplace_back(); auto &entry = params.back(); entry.id = crc.copy().addString(key, true).done(); entry.steppedParam = ¶m; return entry.steppedInfo.emplace(param); } }; void scanParams() { params.clear(); ParamScanner scanner{params}; effect.state(scanner); for (auto &entry : params) entry.setClapInfo(); // Listen for changes from the effect UI effect.paramListenerRange = [&](stfx::web::ParamContext context, double value){ params[context.index].hostValueSent.clear(); if (hostParams) hostParams->request_flush(host); }; effect.paramListenerGesture = [&](stfx::web::ParamContext context, bool gesture){ if (gesture) { params[context.index].hostStartGestureSent.clear(); } else { params[context.index].hostStopGestureSent.clear(); } if (hostParams) hostParams->request_flush(host); }; effect.paramListenerStepped = [&](stfx::web::ParamContext context, int value){ params[context.index].hostValueSent.clear(); if (hostParams) hostParams->request_flush(host); }; } void sendParamEvents(const clap_output_events *events) { for (auto ¶m : params) { auto sendGesture = [&](uint16_t eventType){ clap_event_param_gesture event{ .header={ .size=sizeof(event), .time=0, .space_id=CLAP_CORE_EVENT_SPACE_ID, .type=eventType }, .param_id=param.id, }; // Not *super* bothered if this fails events->try_push(events, &event.header); }; if (!param.hostStartGestureSent.test_and_set()) { sendGesture(CLAP_EVENT_PARAM_GESTURE_BEGIN); } if (!param.hostValueSent.test_and_set()) { clap_event_param_value event{ .header={ .size=sizeof(event), .time=0, .space_id=CLAP_CORE_EVENT_SPACE_ID, .type=CLAP_EVENT_PARAM_VALUE }, .param_id=param.id, .cookie=param.cookie, .note_id=-1, .port_index=-1, .channel=-1, .key=-1 }; if (param.rangeParam) { event.value = param.rangeInfo->toUnit((double)*param.rangeParam); } else { event.value = (int)*param.steppedParam; } if (!events->try_push(events, &event.header)) { // failed, try again later param.hostValueSent.clear(); continue; } } if (!param.hostStopGestureSent.test_and_set()) { sendGesture(CLAP_EVENT_PARAM_GESTURE_END); } } } // CLAP parameter methods static uint32_t params_count(const clap_plugin *obj) { auto &plugin = *(Plugin *)obj; return plugin.params.size(); } static bool params_get_info(const clap_plugin *obj, uint32_t index, clap_param_info *info) { auto &plugin = *(Plugin *)obj; if (index >= plugin.params.size()) return false; *info = plugin.params[index]; return true; } static bool params_get_value(const clap_plugin *obj, clap_id paramId, double *value) { auto &plugin = *(Plugin *)obj; for (auto ¶m : plugin.params) { if (param.id == paramId) { if (param.rangeParam) { *value = param.rangeInfo->toUnit((double)*param.rangeParam); } else { *value = (int)*param.steppedParam; } param.hostValueSent.test_and_set(); return true; } } return false; } static bool params_value_to_text(const clap_plugin *obj, clap_id paramId, double value, char *text, uint32_t textCapacity) { auto &plugin = *(Plugin *)obj; for (auto ¶m : plugin.params) { if (param.id == paramId) { if (param.rangeParam) { auto str = param.rangeInfo->toString(param.rangeInfo->fromUnit(value)); std::strncpy(text, str.c_str(), textCapacity); } else { auto str = param.steppedInfo->toString(int(std::round(value))); std::strncpy(text, str.c_str(), textCapacity); } return true; } } return false; } static bool params_text_to_value(const clap_plugin *obj, clap_id paramId, const char *text, double *value) { auto &plugin = *(Plugin *)obj; for (auto ¶m : plugin.params) { if (param.id == paramId) { std::string str = text; if (param.rangeParam) { *value = param.rangeInfo->toUnit(param.rangeInfo->fromString(str)); } else { *value = param.steppedInfo->fromString(str); } return true; } } return false; } static void params_flush(const clap_plugin *obj, const clap_input_events *inEvents, const clap_output_events *outEvents) { auto &plugin = *(Plugin *)obj; auto count = inEvents->size(inEvents); for (uint32_t i = 0; i < count; ++i) { auto *header = inEvents->get(inEvents, i); plugin.processEvent(header); } plugin.sendParamEvents(outEvents); } // CLAP audio port methods static uint32_t audio_ports_count(const clap_plugin *obj, bool inputPorts) { auto &plugin = *(Plugin *)obj; if (!plugin.isConfigured) { plugin.isConfigured = plugin.effect.configurePersistent(); if (!plugin.isConfigured) return 0; } auto &config = plugin.effect.config; if (inputPorts) { return config.auxInputs.size() + (config.inputChannels > 0); } else { return config.auxOutputs.size() + (config.outputChannels > 0); } } static bool audio_ports_get(const clap_plugin *obj, uint32_t index, bool inputPorts, clap_audio_port_info *info) { auto &plugin = *(Plugin *)obj; if (!plugin.isConfigured) return false; auto &config = plugin.effect.config; clap_id portIdBase = (inputPorts ? 0x1000000 : 0x2000000); auto main = uint32_t(inputPorts ? config.inputChannels : config.outputChannels); auto &aux = (inputPorts ? config.auxInputs : config.auxOutputs); auto auxIndex = index; if (main) { if (index == 0) { *info = { .id=portIdBase, .name={'m', 'a', 'i', 'n'}, .flags=CLAP_AUDIO_PORT_IS_MAIN, .channel_count=main, .port_type=nullptr, .in_place_pair=CLAP_INVALID_ID }; return true; } --auxIndex; } if (auxIndex < aux.size()) { *info = { .id=portIdBase + index, .name={'a', 'u', 'x'}, .flags=CLAP_AUDIO_PORT_IS_MAIN, .channel_count=main, .port_type=nullptr, .in_place_pair=CLAP_INVALID_ID }; if (aux.size() > 1) { info->name[3] = '1' + auxIndex; } return true; } return false; } static bool writeToStream(const unsigned char *bytes, size_t length, const clap_ostream *stream) { size_t index = 0; while (index < length) { size_t remaining = length - index; auto written = stream->write(stream, bytes + index, remaining); if (written <= 0) return false; index += written; } return true; } static bool fillFromStream(const clap_istream *stream, std::vector &buffer) { buffer.resize(0); size_t chunkSize = 1024; size_t length = 0; while (1) { buffer.resize(length + chunkSize); auto read = stream->read(stream, buffer.data() + length, chunkSize); length += read; if (read < 0) { buffer.resize(0); return false; } else if (read == 0) { buffer.resize(length); return true; } chunkSize = buffer.size(); } } std::vector stateBuffer; static bool state_save(const clap_plugin *obj, const clap_ostream *stream) { auto &plugin = *(Plugin *)obj; auto &buffer = plugin.stateBuffer; plugin.effect.saveState(buffer); return writeToStream(buffer.data(), buffer.size(), stream); } static bool state_load(const clap_plugin *obj, const clap_istream *stream) { auto &plugin = *(Plugin *)obj; auto &buffer = plugin.stateBuffer; if (!fillFromStream(stream, buffer)) return false; plugin.effect.loadState(buffer); plugin.sendWebMessages(); // should already be on the main thread return true; } static bool webview1_provide_starting_uri(const clap_plugin_t *obj, char *startingUri, uint32_t capacity) { auto &plugin = *(Plugin *)obj; if (!plugin.hostWebview1) return false; if (!plugin.effect.webPage.size() + 1 > capacity) return false; std::strcpy(startingUri, plugin.effect.webPage.c_str()); return true; } static bool webview1_receive(const clap_plugin_t *obj, const void *buffer, uint32_t size) { auto &plugin = *(Plugin *)obj; plugin.effect.webReceive(buffer, size); // TODO: *double* check we're on the main thread, for safety plugin.sendWebMessages(); return true; } void sendWebMessages() { if (!hostWebview1) return; while (auto *m = effect.getPendingWebMessage()) { hostWebview1->send(host, m->bytes.data(), m->bytes.size()); m->sent(); } } }; }} // namespace extern stfx::clap::Plugins stfxPlugins;