diff --git a/docs/OSC-REMOTE-CONTROL.md b/docs/OSC-REMOTE-CONTROL.md index ccdede85..6174cc1a 100644 --- a/docs/OSC-REMOTE-CONTROL.md +++ b/docs/OSC-REMOTE-CONTROL.md @@ -60,12 +60,133 @@ There is no reply back from Cardinal. #### /param h:moduleId i:paramId f:value Sending a `/param` message will change the parameter value of any loaded module. -(TODO: describe a way to find the module and param id) +(Use `/modules/list` to find module IDs. Param IDs are module-specific and can be listed with `/module/params`.) There is no reply back from Cardinal. NOTE: the first argument must of be int64 type, as regular 32-bit integer is not enough to fit the whole range of values used inside Cardinal/Rack. +#### /modules/list + +Sending a `/modules/list` message will return the list of loaded modules and their IDs as a stream of replies. +Cardinal replies back using `/resp` with "modules" and a JSON payload string for each module, for example: + +```json +{"ok":true,"index":0,"total":4,"module":{"id":123,"plugin":"Fundamental","model":"VCO-1","name":"VCO-1","fullName":"VCV VCO-1"}} +``` + +Clients should collect replies until `index + 1 == total`. +If `total` is `0`, a single reply is sent with `param` set to `null`. + +#### /modules/available + +Sending a `/modules/available` message will return available modules (non-hidden) as a stream of replies. +Cardinal replies back using `/resp` with "modules-available" and a JSON payload string for each module, for example: + +```json +{"ok":true,"index":0,"total":1234,"module":{"plugin":"Fundamental","model":"VCO-1","name":"VCO-1","fullName":"VCV VCO-1"}} +``` + +Clients should collect replies until `index + 1 == total`. +If `total` is `0`, a single reply is sent with `input` set to `null`. + +#### /module/add s:pluginSlug s:modelSlug +#### /module/add s:pluginSlug s:modelSlug i:gridX i:gridY + +Sending a `/module/add` message will create a module by plugin/model slug. +If `gridX/gridY` are provided, the module is placed at that grid position (0,0 is the top-left grid). +If no position is provided, the module is placed at the nearest free slot to avoid overlaps. + +Cardinal replies back using `/resp` with "module-add" and a JSON payload string, for example: + +```json +{"ok":true,"id":123,"plugin":"Fundamental","model":"VCO-1"} +``` + +#### /module/remove h:moduleId + +Sending a `/module/remove` message will remove a module by ID. +Cardinal replies back using `/resp` with "module-remove" and a JSON payload string, for example: + +```json +{"ok":true,"id":123} +``` + +#### /module/info h:moduleId + +Sending a `/module/info` message will return details about a module by ID. +Cardinal replies back using `/resp` with "module-info" and a JSON payload string, for example: + +```json +{"ok":true,"module":{"id":123,"plugin":"Fundamental","model":"VCO-1","name":"VCO-1","fullName":"VCV VCO-1","paramCount":8,"inputCount":4,"outputCount":2,"lightCount":3,"pos":[0,0]}} +``` + +#### /module/params h:moduleId + +Sending a `/module/params` message will return the parameter list (IDs and basic metadata) for a module by ID as a stream of replies. +Cardinal replies back using `/resp` with "module-params" and a JSON payload string for each parameter, for example: + +```json +{"ok":true,"index":0,"total":8,"id":123,"param":{"id":0,"name":"Frequency","min":-4.0,"max":6.0,"default":0.0,"value":0.12}} +``` + +Clients should collect replies until `index + 1 == total`. +If `total` is `0`, a single reply is sent with `output` set to `null`. + +#### /module/inputs h:moduleId + +Sending a `/module/inputs` message will return the input port list (IDs and basic metadata) for a module by ID as a stream of replies. +Cardinal replies back using `/resp` with "module-inputs" and a JSON payload string for each input, for example: + +```json +{"ok":true,"index":0,"total":2,"id":123,"input":{"id":0,"name":"Pitch","fullName":"Pitch input"}} +``` + +Clients should collect replies until `index + 1 == total`. + +#### /module/outputs h:moduleId + +Sending a `/module/outputs` message will return the output port list (IDs and basic metadata) for a module by ID as a stream of replies. +Cardinal replies back using `/resp` with "module-outputs" and a JSON payload string for each output, for example: + +```json +{"ok":true,"index":0,"total":1,"id":123,"output":{"id":0,"name":"Out","fullName":"Out output"}} +``` + +Clients should collect replies until `index + 1 == total`. + +#### /cables/list +#### /cables/list h:moduleId + +Sending a `/cables/list` message will return the list of cables as a stream of replies. +If `moduleId` is provided, only cables connected to that module are included. +Cardinal replies back using `/resp` with "cables" and a JSON payload string for each cable, for example: + +```json +{"ok":true,"index":0,"total":2,"cable":{"id":123,"outputModuleId":1,"outputId":0,"inputModuleId":2,"inputId":0}} +``` + +Clients should collect replies until `index + 1 == total`. +If `total` is `0`, a single reply is sent with `cable` set to `null`. + +#### /cable/add h:outputModuleId i:outputId h:inputModuleId i:inputId + +Sending a `/cable/add` message will connect an output port to an input port. +Cardinal replies back using `/resp` with "cable-add" and a JSON payload string, for example: + +```json +{"ok":true,"id":123,"outputModuleId":1,"outputId":0,"inputModuleId":2,"inputId":0} +``` + +#### /cable/remove h:cableId + +Sending a `/cable/remove` message will remove a cable by ID. +Cardinal replies back using `/resp` with "cable-remove" and a JSON payload string, for example: + +```json +{"ok":true,"id":123} +``` + #### /load b:patch-blob Sending a `/load` message will load the patch file contained in the message. diff --git a/src/CardinalCommon.cpp b/src/CardinalCommon.cpp index f4781dd2..e91339ad 100644 --- a/src/CardinalCommon.cpp +++ b/src/CardinalCommon.cpp @@ -22,13 +22,20 @@ #include #include +#include #include +#include #include +#include #include #include #include #include +#include +#include +#include #include +#include #include #include @@ -363,21 +370,955 @@ static int osc_fallback_handler(const char* const path, const char* const types, return 0; } +#ifdef CARDINAL_INIT_OSC_THREAD +static const char* oscFeatures = ":screenshot:"; +#else +static const char* oscFeatures = ""; +#endif + static int osc_hello_handler(const char*, const char*, lo_arg**, int, const lo_message m, void* const self) { d_stdout("Hello received from OSC, saying hello back to them o/"); const lo_address source = lo_message_get_source(m); const lo_server server = static_cast(self)->oscServer; - // send list of features first + // send list of features first and then hello! + lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "features", oscFeatures); + lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "hello", "ok"); + return 0; +} + +static void osc_send_resp(const lo_message m, Initializer* const init, const char* const msg, const char* const payload) +{ + const lo_address source = lo_message_get_source(m); + const lo_server server = init->oscServer; + lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", msg, payload != nullptr ? payload : ""); +} + +static void osc_send_json(const lo_message m, Initializer* const init, const char* const msg, json_t* const rootJ) +{ + char* const json = json_dumps(rootJ, JSON_COMPACT); + if (json == nullptr) + { + osc_send_resp(m, init, msg, "[]"); + return; + } + osc_send_resp(m, init, msg, json); + std::free(json); +} + +static void osc_send_error(const lo_message m, Initializer* const init, const char* const msg, const char* const error) +{ + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_false()); + json_object_set_new(rootJ, "error", json_string(error != nullptr ? error : "unknown")); + osc_send_json(m, init, msg, rootJ); + json_decref(rootJ); +} + +static int osc_modules_list_handler(const char*, const char*, lo_arg**, int, const lo_message m, void* const self) +{ + d_debug("osc_modules_list_handler()"); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "modules", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + #ifdef CARDINAL_INIT_OSC_THREAD - lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "features", ":screenshot:"); - #else - lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "features", ""); + rack::contextSet(context); + #endif + + const std::vector moduleIds = context->engine->getModuleIds(); + int total = 0; + for (const int64_t moduleId : moduleIds) + { + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr || module->model == nullptr || module->model->plugin == nullptr) + continue; + ++total; + } + + int index = 0; + for (const int64_t moduleId : moduleIds) + { + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr || module->model == nullptr || module->model->plugin == nullptr) + continue; + + json_t* const rootJ = json_object(); + json_t* const moduleJ = json_object(); + + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(index)); + json_object_set_new(rootJ, "total", json_integer(total)); + json_object_set_new(rootJ, "module", moduleJ); + + json_object_set_new(moduleJ, "id", json_integer(moduleId)); + json_object_set_new(moduleJ, "plugin", json_string(module->model->plugin->slug.c_str())); + json_object_set_new(moduleJ, "model", json_string(module->model->slug.c_str())); + json_object_set_new(moduleJ, "name", json_string(module->model->name.c_str())); + json_object_set_new(moduleJ, "fullName", json_string(module->model->getFullName().c_str())); + + osc_send_json(m, init, "modules", rootJ); + json_decref(rootJ); + ++index; + } + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_modules_available_handler(const char*, const char*, lo_arg**, int, const lo_message m, void* const self) +{ + d_debug("osc_modules_available_handler()"); + + Initializer* const init = static_cast(self); + + int total = 0; + for (rack::plugin::Plugin* const plugin : rack::plugin::plugins) + { + if (plugin == nullptr) + continue; + for (rack::plugin::Model* const model : plugin->models) + { + if (model == nullptr || model->hidden) + continue; + ++total; + } + } + + int index = 0; + for (rack::plugin::Plugin* const plugin : rack::plugin::plugins) + { + if (plugin == nullptr) + continue; + + for (rack::plugin::Model* const model : plugin->models) + { + if (model == nullptr || model->hidden) + continue; + + json_t* const rootJ = json_object(); + json_t* const moduleJ = json_object(); + + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(index)); + json_object_set_new(rootJ, "total", json_integer(total)); + json_object_set_new(rootJ, "module", moduleJ); + + json_object_set_new(moduleJ, "plugin", json_string(plugin->slug.c_str())); + json_object_set_new(moduleJ, "model", json_string(model->slug.c_str())); + json_object_set_new(moduleJ, "name", json_string(model->name.c_str())); + json_object_set_new(moduleJ, "fullName", json_string(model->getFullName().c_str())); + + osc_send_json(m, init, "modules-available", rootJ); + json_decref(rootJ); + ++index; + } + } + + return 0; +} + +static int osc_module_add_common(const lo_message m, void* const self, + const char* const pluginSlug, const char* const modelSlug, + const bool hasPos, const int posX, const int posY) +{ + d_debug("osc_module_add_common()"); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-add", "no-plugin-instance"); + return 0; + } + + if (pluginSlug == nullptr || modelSlug == nullptr) + { + osc_send_error(m, init, "module-add", "missing-slug"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::plugin::Model* const model = rack::plugin::getModel(pluginSlug, modelSlug); + if (model == nullptr) + { + osc_send_error(m, init, "module-add", "model-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + rack::engine::Module* const module = model->createModule(); + if (module == nullptr) + { + osc_send_error(m, init, "module-add", "module-create-failed"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + context->engine->addModule(module); + + #ifndef HEADLESS + rack::app::ModuleWidget* const moduleWidget = model->createModuleWidget(module); + if (moduleWidget == nullptr) + { + context->engine->removeModule(module); + delete module; + osc_send_error(m, init, "module-add", "module-widget-create-failed"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + if (context->scene == nullptr || context->scene->rack == nullptr) + { + delete moduleWidget; + osc_send_error(m, init, "module-add", "no-rack-scene"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + if (hasPos) + { + moduleWidget->setGridPosition(rack::math::Vec(posX, posY)); + context->scene->rack->addModule(moduleWidget); + } + else + { + const rack::math::Vec pos = rack::app::RACK_OFFSET; + if (rack::settings::squeezeModules) + { + // Rebuild the old-positions map so the squeeze logic does not write + // through pointers of since-deleted modules. + context->scene->rack->updateModuleOldPositions(); + context->scene->rack->setModulePosSqueeze(moduleWidget, pos); + } + else + context->scene->rack->setModulePosNearest(moduleWidget, pos); + context->scene->rack->addModule(moduleWidget); + } + moduleWidget->loadTemplate(); + #endif + + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "id", json_integer(module->id)); + json_object_set_new(rootJ, "plugin", json_string(pluginSlug)); + json_object_set_new(rootJ, "model", json_string(modelSlug)); + osc_send_json(m, init, "module-add", rootJ); + json_decref(rootJ); + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_module_add_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_add_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 2, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 's', 0); + DISTRHO_SAFE_ASSERT_RETURN(types[1] == 's', 0); + + const char* const pluginSlug = &argv[0]->s; + const char* const modelSlug = &argv[1]->s; + return osc_module_add_common(m, self, pluginSlug, modelSlug, false, 0, 0); +} + +static int osc_module_add_pos_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_add_pos_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 4, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 's', 0); + DISTRHO_SAFE_ASSERT_RETURN(types[1] == 's', 0); + DISTRHO_SAFE_ASSERT_RETURN(types[2] == 'i', 0); + DISTRHO_SAFE_ASSERT_RETURN(types[3] == 'i', 0); + + const char* const pluginSlug = &argv[0]->s; + const char* const modelSlug = &argv[1]->s; + const int posX = argv[2]->i; + const int posY = argv[3]->i; + return osc_module_add_common(m, self, pluginSlug, modelSlug, true, posX, posY); +} + +static int osc_module_remove_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_remove_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-remove", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + #ifndef HEADLESS + if (context->scene != nullptr && context->scene->rack != nullptr) + { + rack::app::ModuleWidget* const mw = context->scene->rack->getModule(moduleId); + if (mw == nullptr) + { + osc_send_error(m, init, "module-remove", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + context->scene->rack->removeModule(mw); + delete mw; + } + else + #endif + { + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr) + { + osc_send_error(m, init, "module-remove", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + // Disconnect cables in headless mode to avoid engine asserts. + const std::vector cableIds = context->engine->getCableIds(); + for (const int64_t cableId : cableIds) + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr) + continue; + if (cable->inputModule == module || cable->outputModule == module) + { + context->engine->removeCable(cable); + delete cable; + } + } + context->engine->removeModule(module); + delete module; + } + + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + osc_send_json(m, init, "module-remove", rootJ); + json_decref(rootJ); + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_module_info_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_info_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-info", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr || module->model == nullptr || module->model->plugin == nullptr) + { + osc_send_error(m, init, "module-info", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + json_t* const rootJ = json_object(); + json_t* const moduleJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "module", moduleJ); + + json_object_set_new(moduleJ, "id", json_integer(moduleId)); + json_object_set_new(moduleJ, "plugin", json_string(module->model->plugin->slug.c_str())); + json_object_set_new(moduleJ, "model", json_string(module->model->slug.c_str())); + json_object_set_new(moduleJ, "name", json_string(module->model->name.c_str())); + json_object_set_new(moduleJ, "fullName", json_string(module->model->getFullName().c_str())); + json_object_set_new(moduleJ, "paramCount", json_integer(module->getNumParams())); + json_object_set_new(moduleJ, "inputCount", json_integer(module->getNumInputs())); + json_object_set_new(moduleJ, "outputCount", json_integer(module->getNumOutputs())); + json_object_set_new(moduleJ, "lightCount", json_integer(module->getNumLights())); + + #ifndef HEADLESS + if (context->scene != nullptr && context->scene->rack != nullptr) + { + if (rack::app::ModuleWidget* const mw = context->scene->rack->getModule(moduleId)) + { + const rack::math::Vec gridPos = mw->getGridPosition(); + json_t* const posJ = json_pack("[i, i]", static_cast(gridPos.x), static_cast(gridPos.y)); + json_object_set_new(moduleJ, "pos", posJ); + } + } + #endif + + osc_send_json(m, init, "module-info", rootJ); + json_decref(rootJ); + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_module_params_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_params_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-params", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr) + { + osc_send_error(m, init, "module-params", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + const int numParams = module->getNumParams(); + if (numParams == 0) + { + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(0)); + json_object_set_new(rootJ, "total", json_integer(0)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "param", json_null()); + osc_send_json(m, init, "module-params", rootJ); + json_decref(rootJ); + } + for (int paramId = 0; paramId < numParams; ++paramId) + { + json_t* const rootJ = json_object(); + json_t* const paramJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(paramId)); + json_object_set_new(rootJ, "total", json_integer(numParams)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "param", paramJ); + + json_object_set_new(paramJ, "id", json_integer(paramId)); + json_object_set_new(paramJ, "value", json_real(context->engine->getParamValue(module, paramId))); + + if (rack::engine::ParamQuantity* const pq = module->getParamQuantity(paramId)) + { + if (! pq->name.empty()) + json_object_set_new(paramJ, "name", json_string(pq->name.c_str())); + if (! pq->unit.empty()) + json_object_set_new(paramJ, "unit", json_string(pq->unit.c_str())); + if (! pq->description.empty()) + json_object_set_new(paramJ, "description", json_string(pq->description.c_str())); + + json_object_set_new(paramJ, "min", json_real(pq->minValue)); + json_object_set_new(paramJ, "max", json_real(pq->maxValue)); + json_object_set_new(paramJ, "default", json_real(pq->defaultValue)); + } + osc_send_json(m, init, "module-params", rootJ); + json_decref(rootJ); + } + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_module_inputs_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_inputs_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-inputs", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr) + { + osc_send_error(m, init, "module-inputs", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + const int numInputs = module->getNumInputs(); + if (numInputs == 0) + { + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(0)); + json_object_set_new(rootJ, "total", json_integer(0)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "input", json_null()); + osc_send_json(m, init, "module-inputs", rootJ); + json_decref(rootJ); + } + for (int portId = 0; portId < numInputs; ++portId) + { + json_t* const rootJ = json_object(); + json_t* const inputJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(portId)); + json_object_set_new(rootJ, "total", json_integer(numInputs)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "input", inputJ); + + json_object_set_new(inputJ, "id", json_integer(portId)); + if (rack::engine::PortInfo* const info = module->getInputInfo(portId)) + { + if (! info->name.empty()) + json_object_set_new(inputJ, "name", json_string(info->name.c_str())); + if (! info->description.empty()) + json_object_set_new(inputJ, "description", json_string(info->description.c_str())); + const std::string fullName = info->getFullName(); + if (! fullName.empty()) + json_object_set_new(inputJ, "fullName", json_string(fullName.c_str())); + } + + osc_send_json(m, init, "module-inputs", rootJ); + json_decref(rootJ); + } + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_module_outputs_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_module_outputs_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "module-outputs", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::engine::Module* const module = context->engine->getModule(moduleId); + if (module == nullptr) + { + osc_send_error(m, init, "module-outputs", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + const int numOutputs = module->getNumOutputs(); + if (numOutputs == 0) + { + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(0)); + json_object_set_new(rootJ, "total", json_integer(0)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "output", json_null()); + osc_send_json(m, init, "module-outputs", rootJ); + json_decref(rootJ); + } + for (int portId = 0; portId < numOutputs; ++portId) + { + json_t* const rootJ = json_object(); + json_t* const outputJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(portId)); + json_object_set_new(rootJ, "total", json_integer(numOutputs)); + json_object_set_new(rootJ, "id", json_integer(moduleId)); + json_object_set_new(rootJ, "output", outputJ); + + json_object_set_new(outputJ, "id", json_integer(portId)); + if (rack::engine::PortInfo* const info = module->getOutputInfo(portId)) + { + if (! info->name.empty()) + json_object_set_new(outputJ, "name", json_string(info->name.c_str())); + if (! info->description.empty()) + json_object_set_new(outputJ, "description", json_string(info->description.c_str())); + const std::string fullName = info->getFullName(); + if (! fullName.empty()) + json_object_set_new(outputJ, "fullName", json_string(fullName.c_str())); + } + + osc_send_json(m, init, "module-outputs", rootJ); + json_decref(rootJ); + } + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_cables_list_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + d_debug("osc_cables_list_handler()"); + DISTRHO_SAFE_ASSERT_RETURN(argc == 0 || argc == 1, 0); + if (argc == 1) + { + DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); + DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); + } + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "cables", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t moduleFilter = (argc == 1) ? argv[0]->h : -1; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + const std::vector cableIds = context->engine->getCableIds(); + int total = 0; + for (const int64_t cableId : cableIds) + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr || cable->inputModule == nullptr || cable->outputModule == nullptr) + continue; + if (moduleFilter >= 0 + && cable->inputModule->id != moduleFilter + && cable->outputModule->id != moduleFilter) + { + continue; + } + ++total; + } + + if (total == 0) + { + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(0)); + json_object_set_new(rootJ, "total", json_integer(0)); + json_object_set_new(rootJ, "cable", json_null()); + if (moduleFilter >= 0) + json_object_set_new(rootJ, "moduleId", json_integer(moduleFilter)); + osc_send_json(m, init, "cables", rootJ); + json_decref(rootJ); + } + + int index = 0; + for (const int64_t cableId : cableIds) + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr || cable->inputModule == nullptr || cable->outputModule == nullptr) + continue; + if (moduleFilter >= 0 + && cable->inputModule->id != moduleFilter + && cable->outputModule->id != moduleFilter) + { + continue; + } + + json_t* const rootJ = json_object(); + json_t* const cableJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "index", json_integer(index)); + json_object_set_new(rootJ, "total", json_integer(total)); + json_object_set_new(rootJ, "cable", cableJ); + if (moduleFilter >= 0) + json_object_set_new(rootJ, "moduleId", json_integer(moduleFilter)); + + json_object_set_new(cableJ, "id", json_integer(cable->id)); + json_object_set_new(cableJ, "outputModuleId", json_integer(cable->outputModule->id)); + json_object_set_new(cableJ, "outputId", json_integer(cable->outputId)); + json_object_set_new(cableJ, "inputModuleId", json_integer(cable->inputModule->id)); + json_object_set_new(cableJ, "inputId", json_integer(cable->inputId)); + + osc_send_json(m, init, "cables", rootJ); + json_decref(rootJ); + ++index; + } + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_cable_add_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "cable-add", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t outputModuleId = argv[0]->h; + const int outputId = argv[1]->i; + const int64_t inputModuleId = argv[2]->h; + const int inputId = argv[3]->i; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + rack::engine::Module* const outputModule = context->engine->getModule(outputModuleId); + rack::engine::Module* const inputModule = context->engine->getModule(inputModuleId); + if (outputModule == nullptr || inputModule == nullptr) + { + osc_send_error(m, init, "cable-add", "module-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + if (outputId < 0 || outputId >= outputModule->getNumOutputs()) + { + osc_send_error(m, init, "cable-add", "output-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + if (inputId < 0 || inputId >= inputModule->getNumInputs()) + { + osc_send_error(m, init, "cable-add", "input-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + + const std::vector cableIds = context->engine->getCableIds(); + for (const int64_t cableId : cableIds) + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr) + continue; + if (cable->inputModule == inputModule && cable->inputId == inputId) + { + osc_send_error(m, init, "cable-add", "input-already-connected"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + } + + rack::engine::Cable* const cable = new rack::engine::Cable; + cable->outputModule = outputModule; + cable->outputId = outputId; + cable->inputModule = inputModule; + cable->inputId = inputId; + context->engine->addCable(cable); + + #ifndef HEADLESS + if (context->scene != nullptr && context->scene->rack != nullptr) + { + try { + rack::app::CableWidget* const cw = new rack::app::CableWidget; + cw->color = context->scene->rack->getNextCableColor(); + cw->setCable(cable); + context->scene->rack->addCable(cw); + } + catch (rack::Exception&) + { + context->engine->removeCable(cable); + delete cable; + osc_send_error(m, init, "cable-add", "cable-widget-create-failed"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + } + #endif + + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "id", json_integer(cable->id)); + json_object_set_new(rootJ, "outputModuleId", json_integer(outputModuleId)); + json_object_set_new(rootJ, "outputId", json_integer(outputId)); + json_object_set_new(rootJ, "inputModuleId", json_integer(inputModuleId)); + json_object_set_new(rootJ, "inputId", json_integer(inputId)); + osc_send_json(m, init, "cable-add", rootJ); + json_decref(rootJ); + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + + return 0; +} + +static int osc_cable_remove_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) +{ + + Initializer* const init = static_cast(self); + CardinalBasePlugin* const plugin = init->remotePluginInstance; + if (plugin == nullptr) + { + osc_send_error(m, init, "cable-remove", "no-plugin-instance"); + return 0; + } + + CardinalPluginContext* const context = plugin->context; + const int64_t cableId = argv[0]->h; + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(context); + #endif + + #ifndef HEADLESS + if (context->scene != nullptr && context->scene->rack != nullptr) + { + if (rack::app::CableWidget* const cw = context->scene->rack->getCable(cableId)) + { + context->scene->rack->removeCable(cw); + delete cw; + } + else + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr) + { + osc_send_error(m, init, "cable-remove", "cable-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + context->engine->removeCable(cable); + delete cable; + } + } + else + #endif + { + rack::engine::Cable* const cable = context->engine->getCable(cableId); + if (cable == nullptr) + { + osc_send_error(m, init, "cable-remove", "cable-not-found"); + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); + #endif + return 0; + } + context->engine->removeCable(cable); + delete cable; + } + + json_t* const rootJ = json_object(); + json_object_set_new(rootJ, "ok", json_true()); + json_object_set_new(rootJ, "id", json_integer(cableId)); + osc_send_json(m, init, "cable-remove", rootJ); + json_decref(rootJ); + + #ifdef CARDINAL_INIT_OSC_THREAD + rack::contextSet(nullptr); #endif - // then finally hello reply - lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "hello", "ok"); return 0; } @@ -704,17 +1645,22 @@ Initializer::Initializer(const CardinalBasePlugin* const plugin, const CardinalB loadSettings(isRealInstance); + #if defined(HAVE_LIBLO) + const char* const portEnv = std::getenv("CARDINAL_REMOTE_HOST_PORT"); #if defined(CARDINAL_INIT_OSC_THREAD) - INFO("Initializing OSC Remote control"); - const char* port; - if (const char* const portEnv = std::getenv("CARDINAL_REMOTE_HOST_PORT")) - port = portEnv; - else - port = CARDINAL_DEFAULT_REMOTE_PORT; - startRemoteServer(port); - #elif defined(HAVE_LIBLO) + // When in headless mode, always boot OSC in a thread with the default port + // unless overridden. + startRemoteServer(portEnv != nullptr ? portEnv : CARDINAL_DEFAULT_REMOTE_PORT); + #endif if (isStandalone()) { - INFO("OSC Remote control is available on request"); + if (portEnv != nullptr) { + // If the envvar is set in headful modes, start now. + if (!startRemoteServer(portEnv)) { + WARN("Failed to start OSC Remote control on port %s", portEnv); + } + } else { + INFO("OSC Remote control is available on request"); + } } else { INFO("OSC Remote control is not available on plugin variants"); } @@ -791,36 +1737,66 @@ void Initializer::loadSettings(const bool isRealInstance) } #ifdef HAVE_LIBLO +struct OscMethod { + const char* path; + const char* types; + lo_method_handler handler; +}; + +static const OscMethod kOscMethods[] = { + {"/hello", "", osc_hello_handler}, + {"/host-param", "if", osc_host_param_handler}, + {"/load", "b", osc_load_handler}, + {"/param", "hif", osc_param_handler}, + {"/modules/list", "", osc_modules_list_handler}, + {"/modules/available", "", osc_modules_available_handler}, + {"/module/add", "ss", osc_module_add_handler}, + {"/module/add", "ssii", osc_module_add_pos_handler}, + {"/module/remove", "h", osc_module_remove_handler}, + {"/module/info", "h", osc_module_info_handler}, + {"/module/params", "h", osc_module_params_handler}, + {"/module/inputs", "h", osc_module_inputs_handler}, + {"/module/outputs", "h", osc_module_outputs_handler}, + {"/cables/list", "", osc_cables_list_handler}, + {"/cables/list", "h", osc_cables_list_handler}, + {"/cable/add", "hihi", osc_cable_add_handler}, + {"/cable/remove", "h", osc_cable_remove_handler}, + #ifdef CARDINAL_INIT_OSC_THREAD + {"/screenshot", "b", osc_screenshot_handler}, + #endif +}; + +template +static void addOscMethods(Server srv, AddFn addFn, void* self) { + for (const auto& m : kOscMethods) + addFn(srv, m.path, m.types, m.handler, self); + addFn(srv, nullptr, nullptr, osc_fallback_handler, nullptr); +} + bool Initializer::startRemoteServer(const char* const port) { #ifdef CARDINAL_INIT_OSC_THREAD if (oscServerThread != nullptr) return true; + INFO("Starting OSC Remote control thread on %s (udp)", port); + if ((oscServerThread = lo_server_thread_new_with_proto(port, LO_UDP, osc_error_handler)) == nullptr) return false; oscServer = lo_server_thread_get_server(oscServerThread); - - lo_server_thread_add_method(oscServerThread, "/hello", "", osc_hello_handler, this); - lo_server_thread_add_method(oscServerThread, "/host-param", "if", osc_host_param_handler, this); - lo_server_thread_add_method(oscServerThread, "/load", "b", osc_load_handler, this); - lo_server_thread_add_method(oscServerThread, "/param", "hif", osc_param_handler, this); - lo_server_thread_add_method(oscServerThread, "/screenshot", "b", osc_screenshot_handler, this); - lo_server_thread_add_method(oscServerThread, nullptr, nullptr, osc_fallback_handler, nullptr); + addOscMethods(oscServerThread, lo_server_thread_add_method, this); lo_server_thread_start(oscServerThread); #else if (oscServer != nullptr) return true; + INFO("Starting OSC Remote control on %s (udp)", port); + if ((oscServer = lo_server_new_with_proto(port, LO_UDP, osc_error_handler)) == nullptr) return false; - lo_server_add_method(oscServer, "/hello", "", osc_hello_handler, this); - lo_server_add_method(oscServer, "/host-param", "if", osc_host_param_handler, this); - lo_server_add_method(oscServer, "/load", "b", osc_load_handler, this); - lo_server_add_method(oscServer, "/param", "hif", osc_param_handler, this); - lo_server_add_method(oscServer, nullptr, nullptr, osc_fallback_handler, nullptr); + addOscMethods(oscServer, lo_server_add_method, this); #endif return true; diff --git a/src/CardinalPlugin.cpp b/src/CardinalPlugin.cpp index 7360c597..215ba5c3 100644 --- a/src/CardinalPlugin.cpp +++ b/src/CardinalPlugin.cpp @@ -313,7 +313,7 @@ class CardinalPlugin : public CardinalBasePlugin context->scene->rackScroll->reset(); } - #ifdef CARDINAL_INIT_OSC_THREAD + #ifdef HAVE_LIBLO fInitializer->remotePluginInstance = this; #endif } @@ -350,6 +350,11 @@ class CardinalPlugin : public CardinalBasePlugin } #ifdef HAVE_LIBLO + bool remoteServerStarted() const override + { + return fInitializer->oscServer != nullptr; + } + bool startRemoteServer(const char* const port) override { if (fInitializer->remotePluginInstance != nullptr) diff --git a/src/CardinalPluginContext.hpp b/src/CardinalPluginContext.hpp index 23f82384..211d00ec 100644 --- a/src/CardinalPluginContext.hpp +++ b/src/CardinalPluginContext.hpp @@ -134,6 +134,7 @@ class CardinalBasePlugin : public Plugin { ~CardinalBasePlugin() override {} #if defined(HAVE_LIBLO) && !CARDINAL_VARIANT_LOADER + virtual bool remoteServerStarted() const = 0; virtual bool startRemoteServer(const char* port) = 0; virtual void stopRemoteServer() = 0; virtual void stepRemoteServer() = 0; diff --git a/src/override/MenuBar.cpp b/src/override/MenuBar.cpp index 77ad6d13..0ba0ef0f 100644 --- a/src/override/MenuBar.cpp +++ b/src/override/MenuBar.cpp @@ -728,10 +728,6 @@ struct ViewButton : MenuButton { struct EngineButton : MenuButton { -#ifdef HAVE_LIBLO - bool remoteServerStarted = false; -#endif - void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; @@ -749,12 +745,11 @@ struct EngineButton : MenuButton { CardinalPluginContext* const context = static_cast(APP); CardinalBasePlugin* const plugin = static_cast(context->plugin); - // const bool remoteServerStarted = this->remoteServerStarted; + const bool remoteServerStarted = plugin->remoteServerStarted(); const std::string remoteControlText = remoteServerStarted ? " " CHECKMARK_STRING : ""; menu->addChild(createMenuItem("Enable OSC remote control", remoteControlText, [=]() { if (remoteServerStarted) { - remoteServerStarted = false; plugin->stopRemoteServer(); return; } @@ -763,8 +758,7 @@ struct EngineButton : MenuButton { if (port == nullptr) return; - if (plugin->startRemoteServer(port)) - remoteServerStarted = true; + plugin->startRemoteServer(port); std::free(port); }); @@ -818,9 +812,9 @@ struct EngineButton : MenuButton { void step() override { MenuButton::step(); - if (remoteServerStarted) { - CardinalPluginContext* const context = static_cast(APP); - CardinalBasePlugin* const plugin = static_cast(context->plugin); + CardinalPluginContext* const context = static_cast(APP); + CardinalBasePlugin* const plugin = static_cast(context->plugin); + if (plugin->remoteServerStarted()) { plugin->stepRemoteServer(); } }