#include #include #include #include #include #include #include #include "ScriptEngine.hpp" #include using namespace rack; Plugin* pluginInstance; // Don't bother deleting this with a destructor. __attribute((init_priority(999))) std::map scriptEngineFactories; ScriptEngine* createScriptEngine(std::string extension) { auto it = scriptEngineFactories.find(extension); if (it == scriptEngineFactories.end()) return NULL; return it->second->createScriptEngine(); } // json_t *settingsToJson() { // json_t *rootJ = json_object(); // json_object_set_new(rootJ, "securityAccepted", json_boolean(securityAccepted)); // return rootJ; // } // void settingsFromJson(json_t *rootJ) { // json_t *securityAcceptedJ = json_object_get(rootJ, "securityAccepted"); // if (securityAcceptedJ) // securityAccepted = json_boolean_value(securityAcceptedJ); // } // void settingsLoad() { // // Load plugin settings // std::string filename = asset::user("VCV-Prototype.json"); // FILE *file = fopen(filename.c_str(), "r"); // if (!file) { // return; // } // DEFER({ // fclose(file); // }); // json_error_t error; // json_t *rootJ = json_loadf(file, 0, &error); // if (rootJ) { // settingsFromJson(rootJ); // json_decref(rootJ); // } // } // void settingsSave() { // json_t *rootJ = settingsToJson(); // std::string filename = asset::user("VCV-Prototype.json"); // FILE *file = fopen(filename.c_str(), "w"); // if (file) { // json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); // fclose(file); // } // json_decref(rootJ); // } struct Prototype : Module { enum ParamIds { ENUMS(KNOB_PARAMS, NUM_ROWS), ENUMS(SWITCH_PARAMS, NUM_ROWS), NUM_PARAMS }; enum InputIds { ENUMS(IN_INPUTS, NUM_ROWS), NUM_INPUTS }; enum OutputIds { ENUMS(OUT_OUTPUTS, NUM_ROWS), NUM_OUTPUTS }; enum LightIds { ENUMS(LIGHT_LIGHTS, NUM_ROWS * 3), ENUMS(SWITCH_LIGHTS, NUM_ROWS * 3), NUM_LIGHTS }; std::string message; std::string path; std::string script; std::string engineName; std::mutex scriptMutex; ScriptEngine* scriptEngine = NULL; int frame = 0; int frameDivider; // This is dynamically allocated to have some protection against script bugs. ProcessBlock* block; int bufferIndex = 0; efsw_watcher efsw = NULL; /** Script that has not yet been approved to load */ std::string securityScript; bool securityRequested = false; bool securityAccepted = false; Prototype() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); for (int i = 0; i < NUM_ROWS; i++) configParam(KNOB_PARAMS + i, 0.f, 1.f, 0.5f, string::f("Knob %d", i + 1)); for (int i = 0; i < NUM_ROWS; i++) configParam(SWITCH_PARAMS + i, 0.f, 1.f, 0.f, string::f("Switch %d", i + 1)); block = new ProcessBlock; setPath(""); } ~Prototype() { setPath(""); delete block; } void onReset() override { setScript(script); } void process(const ProcessArgs& args) override { // Load security-sandboxed script if the security warning message is accepted. if (securityScript != "" && securityAccepted) { setScript(securityScript); securityScript = ""; } // Frame divider for reducing sample rate if (++frame < frameDivider) return; frame = 0; // Clear outputs if no script is running if (!scriptEngine) { for (int i = 0; i < NUM_ROWS; i++) for (int c = 0; c < 3; c++) lights[LIGHT_LIGHTS + i * 3 + c].setBrightness(0.f); for (int i = 0; i < NUM_ROWS; i++) for (int c = 0; c < 3; c++) lights[SWITCH_LIGHTS + i * 3 + c].setBrightness(0.f); for (int i = 0; i < NUM_ROWS; i++) outputs[OUT_OUTPUTS + i].setVoltage(0.f); return; } // Inputs for (int i = 0; i < NUM_ROWS; i++) block->inputs[i][bufferIndex] = inputs[IN_INPUTS + i].getVoltage(); // Process block if (++bufferIndex >= block->bufferSize) { std::lock_guard lock(scriptMutex); bufferIndex = 0; // Block settings block->sampleRate = args.sampleRate; block->sampleTime = args.sampleTime; // Params for (int i = 0; i < NUM_ROWS; i++) block->knobs[i] = params[KNOB_PARAMS + i].getValue(); for (int i = 0; i < NUM_ROWS; i++) block->switches[i] = params[SWITCH_PARAMS + i].getValue() > 0.f; float oldKnobs[NUM_ROWS]; std::memcpy(oldKnobs, block->knobs, sizeof(oldKnobs)); // Run ScriptEngine's process function { // Process buffer if (scriptEngine) { if (scriptEngine->process()) { WARN("Script %s process() failed. Stopped script.", path.c_str()); delete scriptEngine; scriptEngine = NULL; return; } } } // Params // Only set params if values were changed by the script. This avoids issues when the user is manipulating them from the UI thread. for (int i = 0; i < NUM_ROWS; i++) { if (block->knobs[i] != oldKnobs[i]) params[KNOB_PARAMS + i].setValue(block->knobs[i]); } // Lights for (int i = 0; i < NUM_ROWS; i++) for (int c = 0; c < 3; c++) lights[LIGHT_LIGHTS + i * 3 + c].setBrightness(block->lights[i][c]); for (int i = 0; i < NUM_ROWS; i++) for (int c = 0; c < 3; c++) lights[SWITCH_LIGHTS + i * 3 + c].setBrightness(block->switchLights[i][c]); } // Outputs for (int i = 0; i < NUM_ROWS; i++) outputs[OUT_OUTPUTS + i].setVoltage(block->outputs[i][bufferIndex]); } void setPath(std::string path) { // Cleanup if (efsw) { efsw_release(efsw); efsw = NULL; } this->path = ""; setScript(""); if (path == "") return; this->path = path; loadPath(); if (this->script == "") return; // Watch file std::string dir = string::directory(path); efsw = efsw_create(false); efsw_addwatch(efsw, dir.c_str(), watchCallback, false, this); efsw_watch(efsw); } void loadPath() { // Read file std::ifstream file; file.exceptions(std::ifstream::failbit | std::ifstream::badbit); try { file.open(path); std::stringstream buffer; buffer << file.rdbuf(); std::string script = buffer.str(); setScript(script); } catch (const std::runtime_error& err) { // Fail silently } } void setScript(std::string script) { std::lock_guard lock(scriptMutex); // Reset script state if (scriptEngine) { delete scriptEngine; scriptEngine = NULL; } this->script = ""; this->engineName = ""; this->message = ""; // Reset process state frameDivider = 32; frame = 0; bufferIndex = 0; // Reset block *block = ProcessBlock(); if (script == "") return; this->script = script; // Create script engine from path extension std::string extension = string::filenameExtension(string::filename(path)); scriptEngine = createScriptEngine(extension); if (!scriptEngine) { message = string::f("No engine for .%s extension", extension.c_str()); return; } scriptEngine->module = this; // Run script if (scriptEngine->run(path, script)) { // Error message should have been set by ScriptEngine delete scriptEngine; scriptEngine = NULL; return; } this->engineName = scriptEngine->getEngineName(); } static void watchCallback(efsw_watcher watcher, efsw_watchid watchid, const char* dir, const char* filename, enum efsw_action action, const char* old_filename, void* param) { Prototype* that = (Prototype*) param; if (action == EFSW_ADD || action == EFSW_DELETE || action == EFSW_MODIFIED || action == EFSW_MOVED) { // Check filename std::string pathFilename = string::filename(that->path); if (pathFilename == filename) { that->loadPath(); } } } json_t* dataToJson() override { json_t* rootJ = json_object(); json_object_set_new(rootJ, "path", json_string(path.c_str())); std::string script = this->script; // If we haven't accepted the security of this script, serialize the security-sandboxed script anyway. if (script == "") script = securityScript; json_object_set_new(rootJ, "script", json_stringn(script.data(), script.size())); return rootJ; } void dataFromJson(json_t* rootJ) override { json_t* pathJ = json_object_get(rootJ, "path"); if (pathJ) { std::string path = json_string_value(pathJ); setPath(path); } // Only get the script string if the script file wasn't found. if (this->path != "" && this->script == "") { WARN("Script file %s not found, using script in patch", this->path.c_str()); json_t* scriptJ = json_object_get(rootJ, "script"); if (scriptJ) { std::string script = std::string(json_string_value(scriptJ), json_string_length(scriptJ)); if (script != "") { // Request security warning message securityAccepted = false; securityRequested = true; securityScript = script; } } } } void loadScriptDialog() { std::string dir = asset::plugin(pluginInstance, "examples"); char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); if (!pathC) { return; } std::string path = pathC; std::free(pathC); setPath(path); } void reloadScript() { loadPath(); } void saveScriptDialog() { if (script == "") return; std::string ext = string::filenameExtension(string::filename(path)); std::string dir = asset::plugin(pluginInstance, "examples"); std::string filename = "Untitled." + ext; char* newPathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), NULL); if (!newPathC) { return; } std::string newPath = newPathC; std::free(newPathC); // Add extension if user didn't specify one std::string newExt = string::filenameExtension(string::filename(newPath)); if (newExt == "") newPath += "." + ext; // Write and close file { std::ofstream f(newPath); f << script; } // Load path so that it reloads and is watched. setPath(newPath); } }; void ScriptEngine::display(const std::string& message) { module->message = message; } void ScriptEngine::setFrameDivider(int frameDivider) { module->frameDivider = std::max(frameDivider, 1); } void ScriptEngine::setBufferSize(int bufferSize) { module->block->bufferSize = clamp(bufferSize, 1, MAX_BUFFER_SIZE); } ProcessBlock* ScriptEngine::getProcessBlock() { return module->block; } struct FileChoice : LedDisplayChoice { Prototype* module; void step() override { if (module && module->engineName != "") text = module->engineName; else text = "Script"; text += ": "; if (module && module->path != "") text += string::filename(module->path); else text += "(click to load)"; } void onAction(const event::Action& e) override { module->loadScriptDialog(); } }; struct MessageChoice : LedDisplayChoice { Prototype* module; void step() override { text = module ? module->message : ""; } void draw(const DrawArgs& args) override { nvgScissor(args.vg, RECT_ARGS(args.clipBox)); if (font->handle >= 0) { nvgFillColor(args.vg, color); nvgFontFaceId(args.vg, font->handle); nvgTextLetterSpacing(args.vg, 0.0); nvgTextLineHeight(args.vg, 1.08); nvgFontSize(args.vg, 12); nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x - textOffset.x, text.c_str(), NULL); } nvgResetScissor(args.vg); } }; struct PrototypeDisplay : LedDisplay { PrototypeDisplay() { box.size = mm2px(Vec(69.879, 27.335)); } void setModule(Prototype* module) { FileChoice* fileChoice = new FileChoice; fileChoice->box.size.x = box.size.x; fileChoice->module = module; addChild(fileChoice); LedDisplaySeparator* fileSeparator = new LedDisplaySeparator; fileSeparator->box.size.x = box.size.x; fileSeparator->box.pos = fileChoice->box.getBottomLeft(); addChild(fileSeparator); MessageChoice* messageChoice = new MessageChoice; messageChoice->box.pos = fileChoice->box.getBottomLeft(); messageChoice->box.size.x = box.size.x; messageChoice->box.size.y = box.size.y - messageChoice->box.pos.y; messageChoice->module = module; addChild(messageChoice); } }; struct LoadScriptItem : MenuItem { Prototype* module; void onAction(const event::Action& e) override { module->loadScriptDialog(); } }; struct ReloadScriptItem : MenuItem { Prototype* module; void onAction(const event::Action& e) override { module->reloadScript(); } }; struct SaveScriptItem : MenuItem { Prototype* module; void onAction(const event::Action& e) override { module->saveScriptDialog(); } }; struct PrototypeWidget : ModuleWidget { PrototypeWidget(Prototype* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Prototype.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(8.099, 64.401)), module, Prototype::KNOB_PARAMS + 0)); addParam(createParamCentered(mm2px(Vec(20.099, 64.401)), module, Prototype::KNOB_PARAMS + 1)); addParam(createParamCentered(mm2px(Vec(32.099, 64.401)), module, Prototype::KNOB_PARAMS + 2)); addParam(createParamCentered(mm2px(Vec(44.099, 64.401)), module, Prototype::KNOB_PARAMS + 3)); addParam(createParamCentered(mm2px(Vec(56.099, 64.401)), module, Prototype::KNOB_PARAMS + 4)); addParam(createParamCentered(mm2px(Vec(68.099, 64.401)), module, Prototype::KNOB_PARAMS + 5)); addParam(createParamCentered(mm2px(Vec(8.099, 80.151)), module, Prototype::SWITCH_PARAMS + 0)); addParam(createParamCentered(mm2px(Vec(20.099, 80.151)), module, Prototype::SWITCH_PARAMS + 1)); addParam(createParamCentered(mm2px(Vec(32.099, 80.151)), module, Prototype::SWITCH_PARAMS + 2)); addParam(createParamCentered(mm2px(Vec(44.099, 80.151)), module, Prototype::SWITCH_PARAMS + 3)); addParam(createParamCentered(mm2px(Vec(56.099, 80.151)), module, Prototype::SWITCH_PARAMS + 4)); addParam(createParamCentered(mm2px(Vec(68.099, 80.151)), module, Prototype::SWITCH_PARAMS + 5)); addInput(createInputCentered(mm2px(Vec(8.099, 96.025)), module, Prototype::IN_INPUTS + 0)); addInput(createInputCentered(mm2px(Vec(20.099, 96.025)), module, Prototype::IN_INPUTS + 1)); addInput(createInputCentered(mm2px(Vec(32.099, 96.025)), module, Prototype::IN_INPUTS + 2)); addInput(createInputCentered(mm2px(Vec(44.099, 96.025)), module, Prototype::IN_INPUTS + 3)); addInput(createInputCentered(mm2px(Vec(56.099, 96.025)), module, Prototype::IN_INPUTS + 4)); addInput(createInputCentered(mm2px(Vec(68.099, 96.025)), module, Prototype::IN_INPUTS + 5)); addOutput(createOutputCentered(mm2px(Vec(8.099, 112.25)), module, Prototype::OUT_OUTPUTS + 0)); addOutput(createOutputCentered(mm2px(Vec(20.099, 112.25)), module, Prototype::OUT_OUTPUTS + 1)); addOutput(createOutputCentered(mm2px(Vec(32.099, 112.25)), module, Prototype::OUT_OUTPUTS + 2)); addOutput(createOutputCentered(mm2px(Vec(44.099, 112.25)), module, Prototype::OUT_OUTPUTS + 3)); addOutput(createOutputCentered(mm2px(Vec(56.099, 112.25)), module, Prototype::OUT_OUTPUTS + 4)); addOutput(createOutputCentered(mm2px(Vec(68.099, 112.25)), module, Prototype::OUT_OUTPUTS + 5)); addChild(createLightCentered>(mm2px(Vec(8.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 0)); addChild(createLightCentered>(mm2px(Vec(20.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 1)); addChild(createLightCentered>(mm2px(Vec(32.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 2)); addChild(createLightCentered>(mm2px(Vec(44.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 3)); addChild(createLightCentered>(mm2px(Vec(56.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 4)); addChild(createLightCentered>(mm2px(Vec(68.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 5)); addChild(createLightCentered>(mm2px(Vec(8.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 0)); addChild(createLightCentered>(mm2px(Vec(20.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 1)); addChild(createLightCentered>(mm2px(Vec(32.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 2)); addChild(createLightCentered>(mm2px(Vec(44.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 3)); addChild(createLightCentered>(mm2px(Vec(56.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 4)); addChild(createLightCentered>(mm2px(Vec(68.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 5)); PrototypeDisplay* display = createWidget(mm2px(Vec(3.16, 14.837))); display->setModule(module); addChild(display); } void appendContextMenu(Menu* menu) override { Prototype* module = dynamic_cast(this->module); menu->addChild(new MenuEntry); LoadScriptItem* loadScriptItem = createMenuItem("Load script"); loadScriptItem->module = module; menu->addChild(loadScriptItem); ReloadScriptItem* reloadScriptItem = createMenuItem("Reload script"); reloadScriptItem->module = module; menu->addChild(reloadScriptItem); SaveScriptItem* saveScriptItem = createMenuItem("Save script as"); saveScriptItem->module = module; menu->addChild(saveScriptItem); } void onPathDrop(const event::PathDrop& e) override { Prototype* module = dynamic_cast(this->module); if (module && e.paths.size() >= 1) { module->setPath(e.paths[0]); } } void step() override { Prototype* module = dynamic_cast(this->module); if (module && module->securityRequested) { if (osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK_CANCEL, "VCV Prototype is requesting to run a script from a patch or module preset. Running Prototype scripts from untrusted sources may compromise your computer and personal information. Proceed and run script?")) { module->securityAccepted = true; } module->securityRequested = false; } ModuleWidget::step(); } }; void init(Plugin* p) { pluginInstance = p; p->addModel(createModel("Prototype")); }