diff --git a/.gitignore b/.gitignore index d22540ef..cb42c66b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /Rack /Rack.exe +/Rack.res /libRack.a /autosave.json /settings.json diff --git a/Makefile b/Makefile index 09ce3d31..668118db 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ ifeq ($(ARCH), lin) -lpthread -lGL -ldl \ $(shell pkg-config --libs gtk+-2.0) \ -Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl - TARGET = Rack + TARGET := Rack endif ifeq ($(ARCH), mac) @@ -31,8 +31,8 @@ ifeq ($(ARCH), mac) LDFLAGS += -stdlib=libc++ -lpthread -ldl \ -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo \ -Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl - TARGET = Rack - BUNDLE = dist/$(TARGET).app + TARGET := Rack + BUNDLE := dist/$(TARGET).app endif ifeq ($(ARCH), win) @@ -42,8 +42,8 @@ ifeq ($(ARCH), win) -lgdi32 -lopengl32 -lcomdlg32 -lole32 \ -Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl \ -Wl,-Bstatic -ljansson -lspeexdsp - TARGET = Rack.exe - OBJECTS = Rack.res + TARGET := Rack.exe + OBJECTS += Rack.res endif @@ -132,8 +132,7 @@ ifeq ($(ARCH), mac) otool -L $(BUNDLE)/Contents/MacOS/$(TARGET) - mkdir -p $(BUNDLE)/Contents/Resources/plugins - cp -R plugins/Fundamental/dist/Fundamental $(BUNDLE)/Contents/Resources/plugins + cp plugins/Fundamental/dist/Fundamental-*.zip $(BUNDLE)/Contents/Resources/Fundamental.zip # Make DMG image cd dist && ln -s /Applications Applications cd dist && hdiutil create -srcfolder . -volname Rack -ov -format UDZO Rack-$(VERSION)-$(ARCH).dmg @@ -157,8 +156,7 @@ ifeq ($(ARCH), win) cp dep/bin/librtaudio.dll dist/Rack/ cp dep/bin/libcrypto-1_1-x64.dll dist/Rack/ cp dep/bin/libssl-1_1-x64.dll dist/Rack/ - mkdir -p dist/Rack/plugins - cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ + cp plugins/Fundamental/dist/Fundamental-*.zip dist/Rack/Fundamental.zip # Make ZIP cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack # Make NSIS installer @@ -180,17 +178,16 @@ ifeq ($(ARCH), lin) cp dep/lib/librtmidi.so.4 dist/Rack/ cp dep/lib/libssl.so.1.1 dist/Rack/ cp dep/lib/libcrypto.so.1.1 dist/Rack/ - mkdir -p dist/Rack/plugins - cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ + cp plugins/Fundamental/dist/Fundamental-*.zip dist/Rack/Fundamental.zip # Make ZIP cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack endif # Rack SDK distribution mkdir -p dist/Rack-SDK - cp -R LICENSE* res dist/Rack-SDK/ - cp -R include dist/Rack-SDK/ + cp LICENSE* dist/Rack-SDK/ cp *.mk dist/Rack-SDK/ + cp -R include dist/Rack-SDK/ mkdir -p dist/Rack-SDK/dep/ cp -R dep/include dist/Rack-SDK/dep/ ifeq ($(ARCH), win) @@ -200,7 +197,7 @@ endif # Obviously this will only work if you have the private keys to my server -UPLOAD_URL = vortico@vcvrack.com:files/ +UPLOAD_URL := vortico@vcvrack.com:files/ upload: dist distplugins ifeq ($(ARCH), mac) rsync dist/*.dmg $(UPLOAD_URL) -zP diff --git a/README.md b/README.md index c3dd2689..5ad519b0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ pacman -S git make tar unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake autocon On Arch Linux: ``` -pacman -S git gcc make cmake tar unzip curl +pacman -S git gcc make cmake tar unzip zip curl ``` Other distro build instructions coming soon. @@ -51,7 +51,7 @@ Clone this repository with `git clone https://github.com/VCVRack/Rack.git` and ` Make sure there are no spaces in your path, as this breaks many build systems. The `master` branch contains the latest public code and breaks its plugin [API](https://en.wikipedia.org/wiki/Application_programming_interface) and [ABI](https://en.wikipedia.org/wiki/Application_binary_interface) frequently. -If you wish to build a previous version of Rack which is API/ABI-compatible with an official Rack release, check out the desired branch with `git checkout v0.5` for example. +If you wish to build a version of Rack which is API/ABI-compatible with an official Rack release, check out the desired branch with `git checkout v0.5` for example. Clone submodules. diff --git a/dep.mk b/dep.mk index 8c462830..1ee1c7b4 100644 --- a/dep.mk +++ b/dep.mk @@ -28,5 +28,10 @@ $(DEPS): export CXXFLAGS = $(DEP_CXXFLAGS) $(DEPS): export LDFLAGS = $(DEP_LDFLAGS) dep: $(DEPS) + @echo + @echo "#######################################" + @echo "# All dependencies built successfully #" + @echo "#######################################" + @echo .PHONY: dep diff --git a/include/ui.hpp b/include/ui.hpp index 39708aab..981b938e 100644 --- a/include/ui.hpp +++ b/include/ui.hpp @@ -14,7 +14,7 @@ namespace rack { //////////////////// /** Positions children in a row/column based on their widths/heights */ -struct SequentialLayout : virtual Widget { +struct SequentialLayout : VirtualWidget { enum Orientation { HORIZONTAL_ORIENTATION, VERTICAL_ORIENTATION, diff --git a/include/util/common.hpp b/include/util/common.hpp index 4f392459..735e686d 100644 --- a/include/util/common.hpp +++ b/include/util/common.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -15,23 +16,28 @@ // Handy macros //////////////////// -/** Surrounds raw text with quotes +/** Concatenates two literals or two macros Example: - printf("Hello " STRINGIFY(world)) -will expand to - printf("Hello " "world") -and of course the C++ lexer/parser will then concatenate the string literals + #define COUNT 42 + CONCAT(myVariable, COUNT) +expands to + myVariable42 */ -#define STRINGIFY(x) #x -/** Converts a macro to a string literal +#define CONCAT_LITERAL(x, y) x ## y +#define CONCAT(x, y) CONCAT_LITERAL(x, y) + +/** Surrounds raw text with quotes Example: #define NAME "world" printf("Hello " TOSTRING(NAME)) -will expand to +expands to printf("Hello " "world") +and of course the C++ lexer/parser then concatenates the string literals. */ -#define TOSTRING(x) STRINGIFY(x) +#define TOSTRING_LITERAL(x) #x +#define TOSTRING(x) TOSTRING_LITERAL(x) +/** Produces the length of a static array in number of elements */ #define LENGTHOF(arr) (sizeof(arr) / sizeof((arr)[0])) /** Reserve space for `count` enums starting with `name`. @@ -95,10 +101,7 @@ DeferWrapper deferWrapper(F f) { return DeferWrapper(f); } -#define DEFER_1(x, y) x##y -#define DEFER_2(x, y) DEFER_1(x, y) -#define DEFER_3(x) DEFER_2(x, __COUNTER__) -#define defer(code) auto DEFER_3(_defer_) = deferWrapper([&]() code) +#define defer(code) auto CONCAT(x, __COUNTER__) = deferWrapper([&]() code) //////////////////// // Random number generator @@ -123,34 +126,42 @@ inline float DEPRECATED randomf() {return randomUniform();} /** Converts a printf format string and optional arguments into a std::string */ std::string stringf(const char *format, ...); -std::string lowercase(std::string s); -std::string uppercase(std::string s); +std::string stringLowercase(std::string s); +std::string stringUppercase(std::string s); /** Truncates and adds "..." to a string, not exceeding `len` characters */ -std::string ellipsize(std::string s, size_t len); -bool startsWith(std::string str, std::string prefix); +std::string stringEllipsize(std::string s, size_t len); +bool stringStartsWith(std::string str, std::string prefix); +bool stringEndsWith(std::string str, std::string suffix); -std::string extractDirectory(std::string path); -std::string extractFilename(std::string path); -std::string extractExtension(std::string path); +/** Extracts portions of a path */ +std::string stringDirectory(std::string path); +std::string stringFilename(std::string path); +std::string stringExtension(std::string path); //////////////////// // Operating-system specific utilities // system.cpp //////////////////// +std::vector systemListEntries(std::string path); +bool systemIsFile(std::string path); +bool systemIsDirectory(std::string path); +void systemCopy(std::string srcPath, std::string destPath); + /** Opens a URL, also happens to work with PDFs and folders. Shell injection is possible, so make sure the URL is trusted or hard coded. May block, so open in a new thread. */ -void openBrowser(std::string url); +void systemOpenBrowser(std::string url); //////////////////// // Debug logger // logger.cpp //////////////////// -extern FILE *gLogFile; +void loggerInit(); +void loggerDestroy(); void debug(const char *format, ...); void info(const char *format, ...); void warn(const char *format, ...); diff --git a/plugin.mk b/plugin.mk index 1493c0e9..e1b78bcb 100644 --- a/plugin.mk +++ b/plugin.mk @@ -15,17 +15,17 @@ include $(RACK_DIR)/arch.mk ifeq ($(ARCH), lin) LDFLAGS += -shared - TARGET = plugin.so + TARGET := plugin.so endif ifeq ($(ARCH), mac) LDFLAGS += -shared -undefined dynamic_lookup - TARGET = plugin.dylib + TARGET := plugin.dylib endif ifeq ($(ARCH), win) LDFLAGS += -shared -L$(RACK_DIR) -lRack - TARGET = plugin.dll + TARGET := plugin.dll endif diff --git a/src/Core/QuadMIDIToCVInterface.cpp b/src/Core/QuadMIDIToCVInterface.cpp index dca10b66..ff04101d 100644 --- a/src/Core/QuadMIDIToCVInterface.cpp +++ b/src/Core/QuadMIDIToCVInterface.cpp @@ -1,5 +1,6 @@ #include "Core.hpp" #include "midi.hpp" +#include "dsp/digital.hpp" #include @@ -26,6 +27,9 @@ struct QuadMIDIToCVInterface : Module { enum PolyMode { ROTATE_MODE, + // Added REUSE option that reuses a channel when receiving the same note. + // Good when using sustain pedal so it doesn't "stack" unisons ... not sure this is the best name but it is descriptive... + REUSE_MODE, RESET_MODE, REASSIGN_MODE, UNISON_MODE, @@ -39,13 +43,17 @@ struct QuadMIDIToCVInterface : Module { }; NoteData noteData[128]; - std::vector heldNotes; + // cachedNotes : UNISON_MODE and REASSIGN_MODE cache all played notes. The other polyModes cache stolen notes (after the 4th one). + std::vector cachedNotes; uint8_t notes[4]; bool gates[4]; + // gates set to TRUE by pedal and current gate. FALSE by pedal. + bool pedalgates[4]; bool pedal; int rotateIndex; + int stealIndex; - QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), heldNotes(128) { + QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), cachedNotes(128) { onReset(); } @@ -70,71 +78,107 @@ struct QuadMIDIToCVInterface : Module { for (int i = 0; i < 4; i++) { notes[i] = 60; gates[i] = false; + pedalgates[i] = false; } pedal = false; - rotateIndex = 0; + rotateIndex = -1; + cachedNotes.clear(); } - void pressNote(uint8_t note) { - // Remove existing similar note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); - // Push note - heldNotes.push_back(note); + int getPolyIndex(int nowIndex) { + for (int i = 0; i < 4; i++) { + nowIndex++; + if (nowIndex > 3) + nowIndex = 0; + if (!(gates[nowIndex] || pedalgates[nowIndex])) { + stealIndex = nowIndex; + return nowIndex; + } + } + // All taken = steal (stealIndex always rotates) + stealIndex++; + if (stealIndex > 3) + stealIndex = 0; + if ((polyMode < REASSIGN_MODE) && (gates[stealIndex])) + cachedNotes.push_back(notes[stealIndex]); + return stealIndex; + } + void pressNote(uint8_t note) { // Set notes and gates switch (polyMode) { case ROTATE_MODE: { + rotateIndex = getPolyIndex(rotateIndex); } break; - case RESET_MODE: { + case REUSE_MODE: { + bool reuse = false; + for (int i = 0; i < 4; i++) { + if (notes[i] == note) { + rotateIndex = i; + reuse = true; + break; + } + } + if (!reuse) + rotateIndex = getPolyIndex(rotateIndex); + } break; + case RESET_MODE: { + rotateIndex = getPolyIndex(-1); } break; case REASSIGN_MODE: { - + cachedNotes.push_back(note); + rotateIndex = getPolyIndex(-1); } break; case UNISON_MODE: { + cachedNotes.push_back(note); for (int i = 0; i < 4; i++) { notes[i] = note; gates[i] = true; + pedalgates[i] = pedal; + // reTrigger[i].trigger(1e-3); } + return; } break; default: break; } + // Set notes and gates + // if (gates[rotateIndex] || pedalgates[rotateIndex]) + // reTrigger[rotateIndex].trigger(1e-3); + notes[rotateIndex] = note; + gates[rotateIndex] = true; + pedalgates[rotateIndex] = pedal; } void releaseNote(uint8_t note) { // Remove the note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); - // Hold note if pedal is pressed - if (pedal) - return; - // Set last note - switch (polyMode) { - case ROTATE_MODE: { - - } break; - - case RESET_MODE: { - - } break; + auto it = std::find(cachedNotes.begin(), cachedNotes.end(), note); + if (it != cachedNotes.end()) + cachedNotes.erase(it); + switch (polyMode) { case REASSIGN_MODE: { - + for (int i = 0; i < 4; i++) { + if (i < (int) cachedNotes.size()) { + if (!pedalgates[i]) + notes[i] = cachedNotes[i]; + pedalgates[i] = pedal; + } + else { + gates[i] = false; + } + } } break; case UNISON_MODE: { - if (!heldNotes.empty()) { - auto it2 = heldNotes.end(); - it2--; + if (!cachedNotes.empty()) { + uint8_t backnote = cachedNotes.back(); for (int i = 0; i < 4; i++) { - notes[i] = *it2; + notes[i] = backnote; gates[i] = true; } } @@ -145,18 +189,57 @@ struct QuadMIDIToCVInterface : Module { } } break; - default: break; + // default ROTATE_MODE REUSE_MODE RESET_MODE + default: { + for (int i = 0; i < 4; i++) { + if (notes[i] == note) { + if (pedalgates[i]) { + gates[i] = false; + } + else if (!cachedNotes.empty()) { + notes[i] = cachedNotes.back(); + cachedNotes.pop_back(); + } + else { + gates[i] = false; + } + } + } + } break; } - } void pressPedal() { pedal = true; + for (int i = 0; i < 4; i++) { + pedalgates[i] = gates[i]; + } } void releasePedal() { pedal = false; - releaseNote(255); + // When pedal is off, recover notes for pressed keys (if any) after they were already being "cycled" out by pedal-sustained notes. + for (int i = 0; i < 4; i++) { + pedalgates[i] = false; + if (!cachedNotes.empty()) { + if (polyMode < REASSIGN_MODE) { + notes[i] = cachedNotes.back(); + cachedNotes.pop_back(); + gates[i] = true; + } + } + } + if (polyMode == REASSIGN_MODE) { + for (int i = 0; i < 4; i++) { + if (i < (int) cachedNotes.size()) { + notes[i] = cachedNotes[i]; + gates[i] = true; + } + else { + gates[i] = false; + } + } + } } void step() override { @@ -167,14 +250,19 @@ struct QuadMIDIToCVInterface : Module { for (int i = 0; i < 4; i++) { uint8_t lastNote = notes[i]; + uint8_t lastGate = (gates[i] || pedalgates[i]); outputs[CV_OUTPUT + i].value = (lastNote - 60) / 12.f; - outputs[GATE_OUTPUT + i].value = gates[i] ? 10.f : 0.f; + outputs[GATE_OUTPUT + i].value = lastGate ? 10.f : 0.f; outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].velocity, 0, 127, 0.f, 10.f); - outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); + outputs[AFTERTOUCH_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); } } void processMessage(MidiMessage msg) { + // filter MIDI channel + if ((midiInput.channel > -1) && (midiInput.channel != msg.channel())) + return; + switch (msg.status()) { // note off case 0x8: { @@ -262,7 +350,13 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { menu->addChild(MenuEntry::create()); menu->addChild(MenuLabel::create("Polyphony mode")); - std::vector polyModeNames = {"Rotate", "Reset", "Reassign", "Unison"}; + std::vector polyModeNames = { + "Rotate", + "Reuse", + "Reset", + "Reassign", + "Unison" + }; for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { PolyphonyItem *item = MenuItem::create(polyModeNames[i], CHECKMARK(module->polyMode == i)); item->module = module; diff --git a/src/app/ModuleBrowser.cpp b/src/app/ModuleBrowser.cpp index 7f71a8da..f17c4c26 100644 --- a/src/app/ModuleBrowser.cpp +++ b/src/app/ModuleBrowser.cpp @@ -18,8 +18,8 @@ static ModelTag sTagFilter = NO_TAG; bool isMatch(std::string s, std::string search) { - s = lowercase(s); - search = lowercase(search); + s = stringLowercase(s); + search = stringLowercase(search); return (s.find(search) != std::string::npos); } diff --git a/src/app/ModuleWidget.cpp b/src/app/ModuleWidget.cpp index fdacb6c4..4fd88670 100644 --- a/src/app/ModuleWidget.cpp +++ b/src/app/ModuleWidget.cpp @@ -114,6 +114,7 @@ void ModuleWidget::fromJson(json_t *rootJ) { json_t *paramJ; json_array_foreach(paramsJ, i, paramJ) { if (legacy && legacy <= 1) { + // Legacy 1 mode // The index in the array we're iterating is the index of the ParamWidget in the params vector. if (i < params.size()) { // Create upgraded version of param JSON object diff --git a/src/app/PluginManagerWidget.cpp b/src/app/PluginManagerWidget.cpp index 914f0faa..7fd7573f 100644 --- a/src/app/PluginManagerWidget.cpp +++ b/src/app/PluginManagerWidget.cpp @@ -53,6 +53,7 @@ struct SyncButton : Button { PluginManagerWidget::PluginManagerWidget() { + box.size.y = BND_WIDGET_HEIGHT; float margin = 5; @@ -62,7 +63,9 @@ PluginManagerWidget::PluginManagerWidget() { struct RegisterButton : Button { void onAction(EventAction &e) override { - std::thread t(openBrowser, "https://vcvrack.com/"); + std::thread t([&]() { + systemOpenBrowser("https://vcvrack.com/"); + }); t.detach(); } }; @@ -126,7 +129,9 @@ PluginManagerWidget::PluginManagerWidget() { struct ManageButton : Button { void onAction(EventAction &e) override { - std::thread t(openBrowser, "https://vcvrack.com/"); + std::thread t([&]() { + systemOpenBrowser("https://vcvrack.com/"); + }); t.detach(); } }; diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index 906f7bb9..980d214d 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -1,7 +1,6 @@ #include "app.hpp" #include "window.hpp" #include "util/request.hpp" -#include "osdialog.h" #include #include @@ -9,27 +8,6 @@ namespace rack { -static std::string newVersion = ""; - - -#if defined(RELEASE) -static void checkVersion() { - json_t *resJ = requestJson(METHOD_GET, gApiHost + "/version", NULL); - - if (resJ) { - json_t *versionJ = json_object_get(resJ, "version"); - if (versionJ) { - const char *version = json_string_value(versionJ); - if (version && strlen(version) > 0 && version != gApplicationVersion) { - newVersion = version; - } - } - json_decref(resJ); - } -} -#endif - - RackScene::RackScene() { scrollWidget = new RackScrollWidget(); { @@ -46,12 +24,6 @@ RackScene::RackScene() { gToolbar = new Toolbar(); addChild(gToolbar); scrollWidget->box.pos.y = gToolbar->box.size.y; - - // Check for new version -#if defined(RELEASE) - std::thread versionThread(checkVersion); - versionThread.detach(); -#endif } void RackScene::step() { @@ -68,17 +40,6 @@ void RackScene::step() { Scene::step(); zoomWidget->box.size = gRackWidget->box.size.mult(zoomWidget->zoom); - - // Version popup message - if (!newVersion.empty()) { - std::string versionMessage = stringf("Rack %s is available.\n\nYou have Rack %s.\n\nWould you like to download the new version on the website?", newVersion.c_str(), gApplicationVersion.c_str()); - if (osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, versionMessage.c_str())) { - std::thread t(openBrowser, "https://vcvrack.com/"); - t.detach(); - windowClose(); - } - newVersion = ""; - } } void RackScene::draw(NVGcontext *vg) { @@ -129,7 +90,7 @@ void RackScene::onHoverKey(EventHoverKey &e) { void RackScene::onPathDrop(EventPathDrop &e) { if (e.paths.size() >= 1) { const std::string& firstPath = e.paths.front(); - if (extractExtension(firstPath) == "vcv") { + if (stringExtension(firstPath) == "vcv") { gRackWidget->loadPatch(firstPath); e.consumed = true; } diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index f9e3917d..11710bed 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -69,7 +69,7 @@ void RackWidget::reset() { } void RackWidget::openDialog() { - std::string dir = lastPath.empty() ? assetLocal("") : extractDirectory(lastPath); + std::string dir = lastPath.empty() ? assetLocal("") : stringDirectory(lastPath); char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); if (path) { loadPatch(path); @@ -88,13 +88,13 @@ void RackWidget::saveDialog() { } void RackWidget::saveAsDialog() { - std::string dir = lastPath.empty() ? assetLocal("") : extractDirectory(lastPath); + std::string dir = lastPath.empty() ? assetLocal("") : stringDirectory(lastPath); char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), "Untitled.vcv", NULL); if (path) { std::string pathStr = path; free(path); - std::string extension = extractExtension(pathStr); + std::string extension = stringExtension(pathStr); if (extension.empty()) { pathStr += ".vcv"; } @@ -225,15 +225,14 @@ void RackWidget::fromJson(json_t *rootJ) { json_t *versionJ = json_object_get(rootJ, "version"); if (versionJ) { version = json_string_value(versionJ); - if (!version.empty() && gApplicationVersion != version) - message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version.c_str(), gApplicationVersion.c_str()); } // Detect old patches with ModuleWidget::params/inputs/outputs indices. // (We now use Module::params/inputs/outputs indices.) int legacy = 0; - if (startsWith(version, "0.3.") || startsWith(version, "0.4.") || startsWith(version, "0.5.") || version == "" || version == "dev") { + if (stringStartsWith(version, "0.3.") || stringStartsWith(version, "0.4.") || stringStartsWith(version, "0.5.") || version == "" || version == "dev") { legacy = 1; + message += "This patch was created with Rack 0.5 or earlier. Saving it will convert it to a Rack 0.6+ patch.\n\n"; } if (legacy) { info("Loading patch using legacy mode %d", legacy); @@ -246,9 +245,10 @@ void RackWidget::fromJson(json_t *rootJ) { size_t moduleId; json_t *moduleJ; json_array_foreach(modulesJ, moduleId, moduleJ) { - // Set legacy property - if (legacy) - json_object_set_new(moduleJ, "legacy", json_integer(legacy)); + // Add "legacy" property if in legacy mode + if (legacy) { + json_object_set(moduleJ, "legacy", json_integer(legacy)); + } json_t *pluginSlugJ = json_object_get(moduleJ, "plugin"); if (!pluginSlugJ) continue; @@ -259,7 +259,7 @@ void RackWidget::fromJson(json_t *rootJ) { Model *model = pluginGetModel(pluginSlug, modelSlug); if (!model) { - message += stringf("Could not find module \"%s\" in plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); + message += stringf("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); continue; } @@ -292,6 +292,8 @@ void RackWidget::fromJson(json_t *rootJ) { Port *outputPort = NULL; Port *inputPort = NULL; if (legacy && legacy <= 1) { + // Legacy 1 mode + // The index of the "ports" array is the index of the Port in the `outputs` and `inputs` vector. outputPort = outputModuleWidget->outputs[outputId]; inputPort = inputModuleWidget->inputs[inputId]; } @@ -324,7 +326,7 @@ void RackWidget::fromJson(json_t *rootJ) { // Display a message if we have something to say if (!message.empty()) { - osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); + osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); } } diff --git a/src/asset.cpp b/src/asset.cpp index 25384e65..13ee0e7e 100644 --- a/src/asset.cpp +++ b/src/asset.cpp @@ -25,7 +25,7 @@ namespace rack { std::string assetGlobal(std::string filename) { std::string dir; -#if defined(RELEASE) +#if RELEASE #if ARCH_MAC CFBundleRef bundle = CFBundleGetMainBundle(); assert(bundle); @@ -53,7 +53,7 @@ std::string assetGlobal(std::string filename) { std::string assetLocal(std::string filename) { std::string dir; -#if defined(RELEASE) +#if RELEASE #if ARCH_MAC // Get home directory struct passwd *pw = getpwuid(getuid()); diff --git a/src/main.cpp b/src/main.cpp index 9443ebeb..6cba8c13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,19 +6,17 @@ #include "settings.hpp" #include "asset.hpp" #include "bridge.hpp" -#include #include "osdialog.h" +#include + using namespace rack; + int main(int argc, char* argv[]) { randomInit(); - -#ifdef RELEASE - std::string logFilename = assetLocal("log.txt"); - gLogFile = fopen(logFilename.c_str(), "w"); -#endif + loggerInit(); info("Rack %s", gApplicationVersion.c_str()); @@ -64,10 +62,7 @@ int main(int argc, char* argv[]) { bridgeDestroy(); engineDestroy(); pluginDestroy(); - -#ifdef RELEASE - fclose(gLogFile); -#endif + loggerDestroy(); return 0; } diff --git a/src/plugin.cpp b/src/plugin.cpp index 0f7725ce..d9c0237e 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -1,5 +1,10 @@ -#include +#include "plugin.hpp" +#include "app.hpp" +#include "asset.hpp" +#include "util/request.hpp" +#include "osdialog.h" +#include #include #include #include @@ -13,7 +18,7 @@ #include #include -#if defined(ARCH_WIN) +#if ARCH_WIN #include #include #define mkdir(_dir, _perms) _mkdir(_dir) @@ -22,14 +27,10 @@ #endif #include -#include "plugin.hpp" -#include "app.hpp" -#include "asset.hpp" -#include "util/request.hpp" - namespace rack { + std::list gPlugins; std::string gToken; @@ -52,8 +53,11 @@ void Plugin::addModel(Model *model) { models.push_back(model); } +//////////////////// +// private API +//////////////////// -static int loadPlugin(std::string path) { +static bool loadPlugin(std::string path) { std::string libraryFilename; #if ARCH_LIN libraryFilename = path + "/" + "plugin.so"; @@ -63,6 +67,12 @@ static int loadPlugin(std::string path) { libraryFilename = path + "/" + "plugin.dylib"; #endif + // Check file existence + if (!systemIsFile(libraryFilename)) { + warn("Plugin file %s does not exist", libraryFilename.c_str()); + return false; + } + // Load dynamic/shared library #if ARCH_WIN SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); @@ -70,14 +80,14 @@ static int loadPlugin(std::string path) { SetErrorMode(0); if (!handle) { int error = GetLastError(); - warn("Failed to load library %s: %d", libraryFilename.c_str(), error); - return -1; + warn("Failed to load library %s: code %d", libraryFilename.c_str(), error); + return false; } -#elif ARCH_LIN || ARCH_MAC +#else void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); if (!handle) { warn("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); - return -1; + return false; } #endif @@ -86,12 +96,12 @@ static int loadPlugin(std::string path) { InitCallback initCallback; #if ARCH_WIN initCallback = (InitCallback) GetProcAddress(handle, "init"); -#elif ARCH_LIN || ARCH_MAC +#else initCallback = (InitCallback) dlsym(handle, "init"); #endif if (!initCallback) { warn("Failed to read init() symbol in %s", libraryFilename.c_str()); - return -2; + return false; } // Construct and initialize Plugin instance @@ -101,94 +111,19 @@ static int loadPlugin(std::string path) { initCallback(plugin); // Reject plugin if slug already exists - for (Plugin *p : gPlugins) { - if (plugin->slug == p->slug) { - warn("Plugin \"%s\" is already loaded, not attempting to load it again", p->slug.c_str()); - // TODO - // Fix memory leak with `plugin` here - return -1; - } + Plugin *oldPlugin = pluginGetPlugin(plugin->slug); + if (oldPlugin) { + warn("Plugin \"%s\" is already loaded, not attempting to load it again", plugin->slug.c_str()); + // TODO + // Fix memory leak with `plugin` here + return false; } // Add plugin to list gPlugins.push_back(plugin); info("Loaded plugin %s", libraryFilename.c_str()); - return 0; -} - -static void loadPlugins(std::string path) { - DIR *dir = opendir(path.c_str()); - if (dir) { - struct dirent *d; - while ((d = readdir(dir))) { - if (d->d_name[0] == '.') - continue; - loadPlugin(path + "/" + d->d_name); - } - closedir(dir); - } -} - -//////////////////// -// plugin helpers -//////////////////// - -static int extractZipHandle(zip_t *za, const char *dir) { - int err = 0; - for (int i = 0; i < zip_get_num_entries(za, 0); i++) { - zip_stat_t zs; - err = zip_stat_index(za, i, 0, &zs); - if (err) - return err; - int nameLen = strlen(zs.name); - - char path[MAXPATHLEN]; - snprintf(path, sizeof(path), "%s/%s", dir, zs.name); - - if (zs.name[nameLen - 1] == '/') { - err = mkdir(path, 0755); - if (err && errno != EEXIST) - return err; - } - else { - zip_file_t *zf = zip_fopen_index(za, i, 0); - if (!zf) - return 1; - - FILE *outFile = fopen(path, "wb"); - if (!outFile) - continue; - - while (1) { - char buffer[4096]; - int len = zip_fread(zf, buffer, sizeof(buffer)); - if (len <= 0) - break; - fwrite(buffer, 1, len, outFile); - } - - err = zip_fclose(zf); - if (err) - return err; - fclose(outFile); - } - } - return 0; -} - -static int extractZip(const char *filename, const char *dir) { - int err = 0; - zip_t *za = zip_open(filename, 0, &err); - if (!za) - return 1; - - if (!err) { - err = extractZipHandle(za, dir); - } - - zip_close(za); - return err; + return true; } static bool syncPlugin(json_t *pluginJ, bool dryRun) { @@ -223,11 +158,11 @@ static bool syncPlugin(json_t *pluginJ, bool dryRun) { json_t *downloadsJ = json_object_get(pluginJ, "downloads"); if (downloadsJ) { -#if defined(ARCH_WIN) +#if ARCH_WIN #define DOWNLOADS_ARCH "win" -#elif defined(ARCH_MAC) +#elif ARCH_MAC #define DOWNLOADS_ARCH "mac" -#elif defined(ARCH_LIN) +#elif ARCH_LIN #define DOWNLOADS_ARCH "lin" #endif json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); @@ -281,19 +216,151 @@ static bool syncPlugin(json_t *pluginJ, bool dryRun) { } } - // Unzip file - int err = extractZip(zipPath.c_str(), pluginsDir.c_str()); - if (!err) { - // Delete zip - remove(zipPath.c_str()); - // Load plugin - // loadPlugin(pluginPath); - } - downloadName = ""; return true; } +static void loadPlugins(std::string path) { + std::string message; + for (std::string pluginPath : systemListEntries(path)) { + if (!systemIsDirectory(pluginPath)) + continue; + if (!loadPlugin(pluginPath)) { + message += stringf("Could not load plugin %s\n", pluginPath.c_str()); + } + } + if (!message.empty()) { + message += "See log for details."; + osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); + } +} + +static int extractZipHandle(zip_t *za, const char *dir) { + int err = 0; + for (int i = 0; i < zip_get_num_entries(za, 0); i++) { + zip_stat_t zs; + err = zip_stat_index(za, i, 0, &zs); + if (err) + return err; + int nameLen = strlen(zs.name); + + char path[MAXPATHLEN]; + snprintf(path, sizeof(path), "%s/%s", dir, zs.name); + + if (zs.name[nameLen - 1] == '/') { + err = mkdir(path, 0755); + if (err && errno != EEXIST) + return err; + } + else { + zip_file_t *zf = zip_fopen_index(za, i, 0); + if (!zf) + return 1; + + FILE *outFile = fopen(path, "wb"); + if (!outFile) + continue; + + while (1) { + char buffer[1<<15]; + int len = zip_fread(zf, buffer, sizeof(buffer)); + if (len <= 0) + break; + fwrite(buffer, 1, len, outFile); + } + + err = zip_fclose(zf); + if (err) + return err; + fclose(outFile); + } + } + return 0; +} + +static int extractZip(const char *filename, const char *path) { + int err = 0; + zip_t *za = zip_open(filename, 0, &err); + if (!za) + return 1; + defer({ + zip_close(za); + }); + if (err) + return err; + + err = extractZipHandle(za, path); + return err; +} + +static void extractPackages(std::string path) { + std::string message; + + for (std::string packagePath : systemListEntries(path)) { + if (stringExtension(packagePath) == "zip") { + info("Extracting package %s", packagePath.c_str()); + // Extract package + if (extractZip(packagePath.c_str(), path.c_str())) { + message += stringf("Could not extract package %s\n", packagePath.c_str()); + continue; + } + // Remove package + remove(packagePath.c_str()); + } + } + if (!message.empty()) { + osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); + } +} + +//////////////////// +// public API +//////////////////// + +void pluginInit() { + tagsInit(); + + // Load core + // This function is defined in core.cpp + Plugin *corePlugin = new Plugin(); + init(corePlugin); + gPlugins.push_back(corePlugin); + + // Get local plugins directory + std::string localPlugins = assetLocal("plugins"); + mkdir(localPlugins.c_str(), 0755); + +#if RELEASE + // Copy Fundamental package to plugins directory if folder does not exist + std::string fundamentalDest = localPlugins + "/Fundamental.zip"; + if (!systemIsDirectory(localPlugins + "/Fundamental") && !systemIsFile(fundamentalDest)) { + systemCopy(assetGlobal("Fundamental.zip"), fundamentalDest); + } +#endif + + // Extract packages and load plugins + extractPackages(localPlugins); + loadPlugins(localPlugins); +} + +void pluginDestroy() { + for (Plugin *plugin : gPlugins) { + // Free library handle +#if ARCH_WIN + if (plugin->handle) + FreeLibrary((HINSTANCE)plugin->handle); +#else + if (plugin->handle) + dlclose(plugin->handle); +#endif + + // For some reason this segfaults. + // It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. + // delete plugin; + } + gPlugins.clear(); +} + bool pluginSync(bool dryRun) { if (gToken.empty()) return false; @@ -378,51 +445,6 @@ bool pluginSync(bool dryRun) { return available; } -//////////////////// -// plugin API -//////////////////// - -void pluginInit() { - tagsInit(); - - // TODO - // If `/plugins/Fundamental` doesn't exist, unzip global Fundamental.zip package into `/plugins` - - // TODO - // Find all ZIP packages in `/plugins` and unzip them. - // Display error if failure - - // Load core - // This function is defined in core.cpp - Plugin *corePlugin = new Plugin(); - init(corePlugin); - gPlugins.push_back(corePlugin); - - // Load plugins from local directory - std::string localPlugins = assetLocal("plugins"); - mkdir(localPlugins.c_str(), 0755); - info("Loading plugins from %s", localPlugins.c_str()); - loadPlugins(localPlugins); -} - -void pluginDestroy() { - for (Plugin *plugin : gPlugins) { - // Free library handle -#if defined(ARCH_WIN) - if (plugin->handle) - FreeLibrary((HINSTANCE)plugin->handle); -#elif defined(ARCH_LIN) || defined(ARCH_MAC) - if (plugin->handle) - dlclose(plugin->handle); -#endif - - // For some reason this segfaults. - // It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. - // delete plugin; - } - gPlugins.clear(); -} - void pluginLogIn(std::string email, std::string password) { json_t *reqJ = json_object(); json_object_set(reqJ, "email", json_string(email.c_str())); @@ -498,6 +520,4 @@ Model *pluginGetModel(std::string pluginSlug, std::string modelSlug) { } - - } // namespace rack diff --git a/src/util/logger.cpp b/src/util/logger.cpp index 1c6d8963..e1e362b2 100644 --- a/src/util/logger.cpp +++ b/src/util/logger.cpp @@ -1,49 +1,66 @@ #include "util/common.hpp" +#include "asset.hpp" #include namespace rack { -FILE *gLogFile = stderr; +static FILE *logFile = stderr; +static auto startTime = std::chrono::high_resolution_clock::now(); + + +void loggerInit() { +#ifdef RELEASE + std::string logFilename = assetLocal("log.txt"); + logFile = fopen(logFilename.c_str(), "w"); +#endif +} + +void loggerDestroy() { +#ifdef RELEASE + fclose(logFile); +#endif +} + +static void printTimestamp() { +} + +static void printLog(const char *type, const char *format, va_list args) { + auto nowTime = std::chrono::high_resolution_clock::now(); + int duration = std::chrono::duration_cast(nowTime - startTime).count(); + printTimestamp(); + fprintf(logFile, "[%.03f %s] ", duration / 1000.0, type); + vfprintf(logFile, format, args); + fprintf(logFile, "\n"); + fflush(logFile); +} void debug(const char *format, ...) { va_list args; va_start(args, format); - fprintf(gLogFile, "[debug] "); - vfprintf(gLogFile, format, args); - fprintf(gLogFile, "\n"); - fflush(gLogFile); + printLog("debug", format, args); va_end(args); } void info(const char *format, ...) { va_list args; va_start(args, format); - fprintf(gLogFile, "[info] "); - vfprintf(gLogFile, format, args); - fprintf(gLogFile, "\n"); - fflush(gLogFile); + printLog("info", format, args); va_end(args); } void warn(const char *format, ...) { va_list args; va_start(args, format); - fprintf(gLogFile, "[warning] "); - vfprintf(gLogFile, format, args); - fprintf(gLogFile, "\n"); - fflush(gLogFile); + printLog("warn", format, args); va_end(args); } void fatal(const char *format, ...) { va_list args; va_start(args, format); - fprintf(gLogFile, "[fatal] "); - vfprintf(gLogFile, format, args); - fprintf(gLogFile, "\n"); - fflush(gLogFile); + printLog("fatal", format, args); va_end(args); } diff --git a/src/util/string.cpp b/src/util/string.cpp index e2a1b77c..05d958b4 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -24,43 +24,47 @@ std::string stringf(const char *format, ...) { return s; } -std::string lowercase(std::string s) { +std::string stringLowercase(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); return s; } -std::string uppercase(std::string s) { +std::string stringUppercase(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::toupper); return s; } -std::string ellipsize(std::string s, size_t len) { +std::string stringEllipsize(std::string s, size_t len) { if (s.size() <= len) return s; else return s.substr(0, len - 3) + "..."; } -bool startsWith(std::string str, std::string prefix) { +bool stringStartsWith(std::string str, std::string prefix) { return str.substr(0, prefix.size()) == prefix; } -std::string extractDirectory(std::string path) { +bool stringEndsWith(std::string str, std::string suffix) { + return str.substr(str.size() - suffix.size(), suffix.size()) == suffix; +} + +std::string stringDirectory(std::string path) { char *pathDup = strdup(path.c_str()); std::string directory = dirname(pathDup); free(pathDup); return directory; } -std::string extractFilename(std::string path) { +std::string stringFilename(std::string path) { char *pathDup = strdup(path.c_str()); std::string filename = basename(pathDup); free(pathDup); return filename; } -std::string extractExtension(std::string path) { - const char *ext = strrchr(path.c_str(), '.'); +std::string stringExtension(std::string path) { + const char *ext = strrchr(stringFilename(path).c_str(), '.'); if (!ext) return ""; return ext + 1; diff --git a/src/util/system.cpp b/src/util/system.cpp index dcc63774..e4ef26ae 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -1,18 +1,81 @@ #include "util/common.hpp" +#include +#include + #if ARCH_WIN -#include -#include + #include + #include #endif namespace rack { -void openBrowser(std::string url) { +std::vector systemListEntries(std::string path) { + std::vector filenames; + DIR *dir = opendir(path.c_str()); + if (dir) { + struct dirent *d; + while ((d = readdir(dir))) { + std::string filename = d->d_name; + if (filename == "." || filename == "..") + continue; + filenames.push_back(path + "/" + filename); + } + closedir(dir); + } + return filenames; +} + +bool systemExists(std::string path) { + struct stat statbuf; + return (stat(path.c_str(), &statbuf) == 0); +} + +bool systemIsFile(std::string path) { + struct stat statbuf; + if (stat(path.c_str(), &statbuf)) + return false; + return S_ISREG(statbuf.st_mode); +} + +bool systemIsDirectory(std::string path) { + struct stat statbuf; + if (stat(path.c_str(), &statbuf)) + return false; + return S_ISDIR(statbuf.st_mode); +} + +void systemCopy(std::string srcPath, std::string destPath) { + // Open files + FILE *source = fopen(srcPath.c_str(), "rb"); + if (!source) return; + defer({ + fclose(source); + }); + FILE *dest = fopen(destPath.c_str(), "wb"); + if (!dest) return; + defer({ + fclose(dest); + }); + // Copy buffer + const int bufferSize = (1<<15); + char buffer[bufferSize]; + while (1) { + size_t size = fread(buffer, 1, bufferSize, source); + if (size == 0) + break; + size = fwrite(buffer, 1, size, dest); + if (size == 0) + break; + } +} + +void systemOpenBrowser(std::string url) { #if ARCH_LIN std::string command = "xdg-open " + url; - (void)system(command.c_str()); + (void) system(command.c_str()); #endif #if ARCH_MAC std::string command = "open " + url; diff --git a/src/window.cpp b/src/window.cpp index 99c9929a..bb72888f 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -439,7 +439,7 @@ void windowRun() { windowTitle += gApplicationVersion; if (!gRackWidget->lastPath.empty()) { windowTitle += " - "; - windowTitle += extractFilename(gRackWidget->lastPath); + windowTitle += stringFilename(gRackWidget->lastPath); } if (windowTitle != lastWindowTitle) { glfwSetWindowTitle(gWindow, windowTitle.c_str());