@@ -1,5 +1,6 @@ | |||||
/Rack | /Rack | ||||
/Rack.exe | /Rack.exe | ||||
/Rack.res | |||||
/libRack.a | /libRack.a | ||||
/autosave.json | /autosave.json | ||||
/settings.json | /settings.json | ||||
@@ -22,7 +22,7 @@ ifeq ($(ARCH), lin) | |||||
-lpthread -lGL -ldl \ | -lpthread -lGL -ldl \ | ||||
$(shell pkg-config --libs gtk+-2.0) \ | $(shell pkg-config --libs gtk+-2.0) \ | ||||
-Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl | -Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl | ||||
TARGET = Rack | |||||
TARGET := Rack | |||||
endif | endif | ||||
ifeq ($(ARCH), mac) | ifeq ($(ARCH), mac) | ||||
@@ -31,8 +31,8 @@ ifeq ($(ARCH), mac) | |||||
LDFLAGS += -stdlib=libc++ -lpthread -ldl \ | LDFLAGS += -stdlib=libc++ -lpthread -ldl \ | ||||
-framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo \ | -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo \ | ||||
-Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl | -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 | endif | ||||
ifeq ($(ARCH), win) | ifeq ($(ARCH), win) | ||||
@@ -42,8 +42,8 @@ ifeq ($(ARCH), win) | |||||
-lgdi32 -lopengl32 -lcomdlg32 -lole32 \ | -lgdi32 -lopengl32 -lcomdlg32 -lole32 \ | ||||
-Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl \ | -Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl \ | ||||
-Wl,-Bstatic -ljansson -lspeexdsp | -Wl,-Bstatic -ljansson -lspeexdsp | ||||
TARGET = Rack.exe | |||||
OBJECTS = Rack.res | |||||
TARGET := Rack.exe | |||||
OBJECTS += Rack.res | |||||
endif | endif | ||||
@@ -132,8 +132,7 @@ ifeq ($(ARCH), mac) | |||||
otool -L $(BUNDLE)/Contents/MacOS/$(TARGET) | 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 | # Make DMG image | ||||
cd dist && ln -s /Applications Applications | cd dist && ln -s /Applications Applications | ||||
cd dist && hdiutil create -srcfolder . -volname Rack -ov -format UDZO Rack-$(VERSION)-$(ARCH).dmg | 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/librtaudio.dll dist/Rack/ | ||||
cp dep/bin/libcrypto-1_1-x64.dll dist/Rack/ | cp dep/bin/libcrypto-1_1-x64.dll dist/Rack/ | ||||
cp dep/bin/libssl-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 | # Make ZIP | ||||
cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | ||||
# Make NSIS installer | # Make NSIS installer | ||||
@@ -180,17 +178,16 @@ ifeq ($(ARCH), lin) | |||||
cp dep/lib/librtmidi.so.4 dist/Rack/ | cp dep/lib/librtmidi.so.4 dist/Rack/ | ||||
cp dep/lib/libssl.so.1.1 dist/Rack/ | cp dep/lib/libssl.so.1.1 dist/Rack/ | ||||
cp dep/lib/libcrypto.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 | # Make ZIP | ||||
cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | ||||
endif | endif | ||||
# Rack SDK distribution | # Rack SDK distribution | ||||
mkdir -p dist/Rack-SDK | 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 *.mk dist/Rack-SDK/ | ||||
cp -R include dist/Rack-SDK/ | |||||
mkdir -p dist/Rack-SDK/dep/ | mkdir -p dist/Rack-SDK/dep/ | ||||
cp -R dep/include dist/Rack-SDK/dep/ | cp -R dep/include dist/Rack-SDK/dep/ | ||||
ifeq ($(ARCH), win) | ifeq ($(ARCH), win) | ||||
@@ -200,7 +197,7 @@ endif | |||||
# Obviously this will only work if you have the private keys to my server | # 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 | upload: dist distplugins | ||||
ifeq ($(ARCH), mac) | ifeq ($(ARCH), mac) | ||||
rsync dist/*.dmg $(UPLOAD_URL) -zP | rsync dist/*.dmg $(UPLOAD_URL) -zP | ||||
@@ -38,7 +38,7 @@ pacman -S git make tar unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake autocon | |||||
On Arch Linux: | 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. | 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. | 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. | 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. | Clone submodules. | ||||
@@ -28,5 +28,10 @@ $(DEPS): export CXXFLAGS = $(DEP_CXXFLAGS) | |||||
$(DEPS): export LDFLAGS = $(DEP_LDFLAGS) | $(DEPS): export LDFLAGS = $(DEP_LDFLAGS) | ||||
dep: $(DEPS) | dep: $(DEPS) | ||||
@echo | |||||
@echo "#######################################" | |||||
@echo "# All dependencies built successfully #" | |||||
@echo "#######################################" | |||||
@echo | |||||
.PHONY: dep | .PHONY: dep |
@@ -14,7 +14,7 @@ namespace rack { | |||||
//////////////////// | //////////////////// | ||||
/** Positions children in a row/column based on their widths/heights */ | /** Positions children in a row/column based on their widths/heights */ | ||||
struct SequentialLayout : virtual Widget { | |||||
struct SequentialLayout : VirtualWidget { | |||||
enum Orientation { | enum Orientation { | ||||
HORIZONTAL_ORIENTATION, | HORIZONTAL_ORIENTATION, | ||||
VERTICAL_ORIENTATION, | VERTICAL_ORIENTATION, | ||||
@@ -8,6 +8,7 @@ | |||||
#include <assert.h> | #include <assert.h> | ||||
#include <string> | #include <string> | ||||
#include <vector> | |||||
#include <condition_variable> | #include <condition_variable> | ||||
#include <mutex> | #include <mutex> | ||||
@@ -15,23 +16,28 @@ | |||||
// Handy macros | // Handy macros | ||||
//////////////////// | //////////////////// | ||||
/** Surrounds raw text with quotes | |||||
/** Concatenates two literals or two macros | |||||
Example: | 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: | Example: | ||||
#define NAME "world" | #define NAME "world" | ||||
printf("Hello " TOSTRING(NAME)) | printf("Hello " TOSTRING(NAME)) | ||||
will expand to | |||||
expands to | |||||
printf("Hello " "world") | 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])) | #define LENGTHOF(arr) (sizeof(arr) / sizeof((arr)[0])) | ||||
/** Reserve space for `count` enums starting with `name`. | /** Reserve space for `count` enums starting with `name`. | ||||
@@ -95,10 +101,7 @@ DeferWrapper<F> deferWrapper(F f) { | |||||
return DeferWrapper<F>(f); | return DeferWrapper<F>(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 | // 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 */ | /** Converts a printf format string and optional arguments into a std::string */ | ||||
std::string stringf(const char *format, ...); | 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 */ | /** 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 | // Operating-system specific utilities | ||||
// system.cpp | // system.cpp | ||||
//////////////////// | //////////////////// | ||||
std::vector<std::string> 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. | /** 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. | Shell injection is possible, so make sure the URL is trusted or hard coded. | ||||
May block, so open in a new thread. | May block, so open in a new thread. | ||||
*/ | */ | ||||
void openBrowser(std::string url); | |||||
void systemOpenBrowser(std::string url); | |||||
//////////////////// | //////////////////// | ||||
// Debug logger | // Debug logger | ||||
// logger.cpp | // logger.cpp | ||||
//////////////////// | //////////////////// | ||||
extern FILE *gLogFile; | |||||
void loggerInit(); | |||||
void loggerDestroy(); | |||||
void debug(const char *format, ...); | void debug(const char *format, ...); | ||||
void info(const char *format, ...); | void info(const char *format, ...); | ||||
void warn(const char *format, ...); | void warn(const char *format, ...); | ||||
@@ -15,17 +15,17 @@ include $(RACK_DIR)/arch.mk | |||||
ifeq ($(ARCH), lin) | ifeq ($(ARCH), lin) | ||||
LDFLAGS += -shared | LDFLAGS += -shared | ||||
TARGET = plugin.so | |||||
TARGET := plugin.so | |||||
endif | endif | ||||
ifeq ($(ARCH), mac) | ifeq ($(ARCH), mac) | ||||
LDFLAGS += -shared -undefined dynamic_lookup | LDFLAGS += -shared -undefined dynamic_lookup | ||||
TARGET = plugin.dylib | |||||
TARGET := plugin.dylib | |||||
endif | endif | ||||
ifeq ($(ARCH), win) | ifeq ($(ARCH), win) | ||||
LDFLAGS += -shared -L$(RACK_DIR) -lRack | LDFLAGS += -shared -L$(RACK_DIR) -lRack | ||||
TARGET = plugin.dll | |||||
TARGET := plugin.dll | |||||
endif | endif | ||||
@@ -1,5 +1,6 @@ | |||||
#include "Core.hpp" | #include "Core.hpp" | ||||
#include "midi.hpp" | #include "midi.hpp" | ||||
#include "dsp/digital.hpp" | |||||
#include <algorithm> | #include <algorithm> | ||||
@@ -26,6 +27,9 @@ struct QuadMIDIToCVInterface : Module { | |||||
enum PolyMode { | enum PolyMode { | ||||
ROTATE_MODE, | 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, | RESET_MODE, | ||||
REASSIGN_MODE, | REASSIGN_MODE, | ||||
UNISON_MODE, | UNISON_MODE, | ||||
@@ -39,13 +43,17 @@ struct QuadMIDIToCVInterface : Module { | |||||
}; | }; | ||||
NoteData noteData[128]; | NoteData noteData[128]; | ||||
std::vector<uint8_t> heldNotes; | |||||
// cachedNotes : UNISON_MODE and REASSIGN_MODE cache all played notes. The other polyModes cache stolen notes (after the 4th one). | |||||
std::vector<uint8_t> cachedNotes; | |||||
uint8_t notes[4]; | uint8_t notes[4]; | ||||
bool gates[4]; | bool gates[4]; | ||||
// gates set to TRUE by pedal and current gate. FALSE by pedal. | |||||
bool pedalgates[4]; | |||||
bool pedal; | bool pedal; | ||||
int rotateIndex; | 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(); | onReset(); | ||||
} | } | ||||
@@ -70,71 +78,107 @@ struct QuadMIDIToCVInterface : Module { | |||||
for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
notes[i] = 60; | notes[i] = 60; | ||||
gates[i] = false; | gates[i] = false; | ||||
pedalgates[i] = false; | |||||
} | } | ||||
pedal = 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 | // Set notes and gates | ||||
switch (polyMode) { | switch (polyMode) { | ||||
case ROTATE_MODE: { | case ROTATE_MODE: { | ||||
rotateIndex = getPolyIndex(rotateIndex); | |||||
} break; | } 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; | } break; | ||||
case REASSIGN_MODE: { | case REASSIGN_MODE: { | ||||
cachedNotes.push_back(note); | |||||
rotateIndex = getPolyIndex(-1); | |||||
} break; | } break; | ||||
case UNISON_MODE: { | case UNISON_MODE: { | ||||
cachedNotes.push_back(note); | |||||
for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
notes[i] = note; | notes[i] = note; | ||||
gates[i] = true; | gates[i] = true; | ||||
pedalgates[i] = pedal; | |||||
// reTrigger[i].trigger(1e-3); | |||||
} | } | ||||
return; | |||||
} break; | } break; | ||||
default: 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) { | void releaseNote(uint8_t note) { | ||||
// Remove the 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: { | 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; | } break; | ||||
case UNISON_MODE: { | 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++) { | for (int i = 0; i < 4; i++) { | ||||
notes[i] = *it2; | |||||
notes[i] = backnote; | |||||
gates[i] = true; | gates[i] = true; | ||||
} | } | ||||
} | } | ||||
@@ -145,18 +189,57 @@ struct QuadMIDIToCVInterface : Module { | |||||
} | } | ||||
} break; | } 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() { | void pressPedal() { | ||||
pedal = true; | pedal = true; | ||||
for (int i = 0; i < 4; i++) { | |||||
pedalgates[i] = gates[i]; | |||||
} | |||||
} | } | ||||
void releasePedal() { | void releasePedal() { | ||||
pedal = false; | 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 { | void step() override { | ||||
@@ -167,14 +250,19 @@ struct QuadMIDIToCVInterface : Module { | |||||
for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
uint8_t lastNote = notes[i]; | uint8_t lastNote = notes[i]; | ||||
uint8_t lastGate = (gates[i] || pedalgates[i]); | |||||
outputs[CV_OUTPUT + i].value = (lastNote - 60) / 12.f; | 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].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) { | void processMessage(MidiMessage msg) { | ||||
// filter MIDI channel | |||||
if ((midiInput.channel > -1) && (midiInput.channel != msg.channel())) | |||||
return; | |||||
switch (msg.status()) { | switch (msg.status()) { | ||||
// note off | // note off | ||||
case 0x8: { | case 0x8: { | ||||
@@ -262,7 +350,13 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||||
menu->addChild(MenuEntry::create()); | menu->addChild(MenuEntry::create()); | ||||
menu->addChild(MenuLabel::create("Polyphony mode")); | menu->addChild(MenuLabel::create("Polyphony mode")); | ||||
std::vector<std::string> polyModeNames = {"Rotate", "Reset", "Reassign", "Unison"}; | |||||
std::vector<std::string> polyModeNames = { | |||||
"Rotate", | |||||
"Reuse", | |||||
"Reset", | |||||
"Reassign", | |||||
"Unison" | |||||
}; | |||||
for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { | for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { | ||||
PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | ||||
item->module = module; | item->module = module; | ||||
@@ -18,8 +18,8 @@ static ModelTag sTagFilter = NO_TAG; | |||||
bool isMatch(std::string s, std::string search) { | 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); | return (s.find(search) != std::string::npos); | ||||
} | } | ||||
@@ -114,6 +114,7 @@ void ModuleWidget::fromJson(json_t *rootJ) { | |||||
json_t *paramJ; | json_t *paramJ; | ||||
json_array_foreach(paramsJ, i, paramJ) { | json_array_foreach(paramsJ, i, paramJ) { | ||||
if (legacy && legacy <= 1) { | 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. | // The index in the array we're iterating is the index of the ParamWidget in the params vector. | ||||
if (i < params.size()) { | if (i < params.size()) { | ||||
// Create upgraded version of param JSON object | // Create upgraded version of param JSON object | ||||
@@ -53,6 +53,7 @@ struct SyncButton : Button { | |||||
PluginManagerWidget::PluginManagerWidget() { | PluginManagerWidget::PluginManagerWidget() { | ||||
box.size.y = BND_WIDGET_HEIGHT; | box.size.y = BND_WIDGET_HEIGHT; | ||||
float margin = 5; | float margin = 5; | ||||
@@ -62,7 +63,9 @@ PluginManagerWidget::PluginManagerWidget() { | |||||
struct RegisterButton : Button { | struct RegisterButton : Button { | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
std::thread t(openBrowser, "https://vcvrack.com/"); | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | t.detach(); | ||||
} | } | ||||
}; | }; | ||||
@@ -126,7 +129,9 @@ PluginManagerWidget::PluginManagerWidget() { | |||||
struct ManageButton : Button { | struct ManageButton : Button { | ||||
void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
std::thread t(openBrowser, "https://vcvrack.com/"); | |||||
std::thread t([&]() { | |||||
systemOpenBrowser("https://vcvrack.com/"); | |||||
}); | |||||
t.detach(); | t.detach(); | ||||
} | } | ||||
}; | }; | ||||
@@ -1,7 +1,6 @@ | |||||
#include "app.hpp" | #include "app.hpp" | ||||
#include "window.hpp" | #include "window.hpp" | ||||
#include "util/request.hpp" | #include "util/request.hpp" | ||||
#include "osdialog.h" | |||||
#include <string.h> | #include <string.h> | ||||
#include <thread> | #include <thread> | ||||
@@ -9,27 +8,6 @@ | |||||
namespace rack { | 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() { | RackScene::RackScene() { | ||||
scrollWidget = new RackScrollWidget(); | scrollWidget = new RackScrollWidget(); | ||||
{ | { | ||||
@@ -46,12 +24,6 @@ RackScene::RackScene() { | |||||
gToolbar = new Toolbar(); | gToolbar = new Toolbar(); | ||||
addChild(gToolbar); | addChild(gToolbar); | ||||
scrollWidget->box.pos.y = gToolbar->box.size.y; | 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() { | void RackScene::step() { | ||||
@@ -68,17 +40,6 @@ void RackScene::step() { | |||||
Scene::step(); | Scene::step(); | ||||
zoomWidget->box.size = gRackWidget->box.size.mult(zoomWidget->zoom); | 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) { | void RackScene::draw(NVGcontext *vg) { | ||||
@@ -129,7 +90,7 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||||
void RackScene::onPathDrop(EventPathDrop &e) { | void RackScene::onPathDrop(EventPathDrop &e) { | ||||
if (e.paths.size() >= 1) { | if (e.paths.size() >= 1) { | ||||
const std::string& firstPath = e.paths.front(); | const std::string& firstPath = e.paths.front(); | ||||
if (extractExtension(firstPath) == "vcv") { | |||||
if (stringExtension(firstPath) == "vcv") { | |||||
gRackWidget->loadPatch(firstPath); | gRackWidget->loadPatch(firstPath); | ||||
e.consumed = true; | e.consumed = true; | ||||
} | } | ||||
@@ -69,7 +69,7 @@ void RackWidget::reset() { | |||||
} | } | ||||
void RackWidget::openDialog() { | 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); | char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); | ||||
if (path) { | if (path) { | ||||
loadPatch(path); | loadPatch(path); | ||||
@@ -88,13 +88,13 @@ void RackWidget::saveDialog() { | |||||
} | } | ||||
void RackWidget::saveAsDialog() { | 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); | char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), "Untitled.vcv", NULL); | ||||
if (path) { | if (path) { | ||||
std::string pathStr = path; | std::string pathStr = path; | ||||
free(path); | free(path); | ||||
std::string extension = extractExtension(pathStr); | |||||
std::string extension = stringExtension(pathStr); | |||||
if (extension.empty()) { | if (extension.empty()) { | ||||
pathStr += ".vcv"; | pathStr += ".vcv"; | ||||
} | } | ||||
@@ -225,15 +225,14 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
json_t *versionJ = json_object_get(rootJ, "version"); | json_t *versionJ = json_object_get(rootJ, "version"); | ||||
if (versionJ) { | if (versionJ) { | ||||
version = json_string_value(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. | // Detect old patches with ModuleWidget::params/inputs/outputs indices. | ||||
// (We now use Module::params/inputs/outputs indices.) | // (We now use Module::params/inputs/outputs indices.) | ||||
int legacy = 0; | 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; | 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) { | if (legacy) { | ||||
info("Loading patch using legacy mode %d", legacy); | info("Loading patch using legacy mode %d", legacy); | ||||
@@ -246,9 +245,10 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
size_t moduleId; | size_t moduleId; | ||||
json_t *moduleJ; | json_t *moduleJ; | ||||
json_array_foreach(modulesJ, moduleId, 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"); | json_t *pluginSlugJ = json_object_get(moduleJ, "plugin"); | ||||
if (!pluginSlugJ) continue; | if (!pluginSlugJ) continue; | ||||
@@ -259,7 +259,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
Model *model = pluginGetModel(pluginSlug, modelSlug); | Model *model = pluginGetModel(pluginSlug, modelSlug); | ||||
if (!model) { | 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; | continue; | ||||
} | } | ||||
@@ -292,6 +292,8 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
Port *outputPort = NULL; | Port *outputPort = NULL; | ||||
Port *inputPort = NULL; | Port *inputPort = NULL; | ||||
if (legacy && legacy <= 1) { | 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]; | outputPort = outputModuleWidget->outputs[outputId]; | ||||
inputPort = inputModuleWidget->inputs[inputId]; | inputPort = inputModuleWidget->inputs[inputId]; | ||||
} | } | ||||
@@ -324,7 +326,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||||
// Display a message if we have something to say | // Display a message if we have something to say | ||||
if (!message.empty()) { | if (!message.empty()) { | ||||
osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); | |||||
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | |||||
} | } | ||||
} | } | ||||
@@ -25,7 +25,7 @@ namespace rack { | |||||
std::string assetGlobal(std::string filename) { | std::string assetGlobal(std::string filename) { | ||||
std::string dir; | std::string dir; | ||||
#if defined(RELEASE) | |||||
#if RELEASE | |||||
#if ARCH_MAC | #if ARCH_MAC | ||||
CFBundleRef bundle = CFBundleGetMainBundle(); | CFBundleRef bundle = CFBundleGetMainBundle(); | ||||
assert(bundle); | assert(bundle); | ||||
@@ -53,7 +53,7 @@ std::string assetGlobal(std::string filename) { | |||||
std::string assetLocal(std::string filename) { | std::string assetLocal(std::string filename) { | ||||
std::string dir; | std::string dir; | ||||
#if defined(RELEASE) | |||||
#if RELEASE | |||||
#if ARCH_MAC | #if ARCH_MAC | ||||
// Get home directory | // Get home directory | ||||
struct passwd *pw = getpwuid(getuid()); | struct passwd *pw = getpwuid(getuid()); | ||||
@@ -6,19 +6,17 @@ | |||||
#include "settings.hpp" | #include "settings.hpp" | ||||
#include "asset.hpp" | #include "asset.hpp" | ||||
#include "bridge.hpp" | #include "bridge.hpp" | ||||
#include <unistd.h> | |||||
#include "osdialog.h" | #include "osdialog.h" | ||||
#include <unistd.h> | |||||
using namespace rack; | using namespace rack; | ||||
int main(int argc, char* argv[]) { | int main(int argc, char* argv[]) { | ||||
randomInit(); | randomInit(); | ||||
#ifdef RELEASE | |||||
std::string logFilename = assetLocal("log.txt"); | |||||
gLogFile = fopen(logFilename.c_str(), "w"); | |||||
#endif | |||||
loggerInit(); | |||||
info("Rack %s", gApplicationVersion.c_str()); | info("Rack %s", gApplicationVersion.c_str()); | ||||
@@ -64,10 +62,7 @@ int main(int argc, char* argv[]) { | |||||
bridgeDestroy(); | bridgeDestroy(); | ||||
engineDestroy(); | engineDestroy(); | ||||
pluginDestroy(); | pluginDestroy(); | ||||
#ifdef RELEASE | |||||
fclose(gLogFile); | |||||
#endif | |||||
loggerDestroy(); | |||||
return 0; | return 0; | ||||
} | } |
@@ -1,5 +1,10 @@ | |||||
#include <stdio.h> | |||||
#include "plugin.hpp" | |||||
#include "app.hpp" | |||||
#include "asset.hpp" | |||||
#include "util/request.hpp" | |||||
#include "osdialog.h" | |||||
#include <stdio.h> | |||||
#include <assert.h> | #include <assert.h> | ||||
#include <string.h> | #include <string.h> | ||||
#include <unistd.h> | #include <unistd.h> | ||||
@@ -13,7 +18,7 @@ | |||||
#include <zip.h> | #include <zip.h> | ||||
#include <jansson.h> | #include <jansson.h> | ||||
#if defined(ARCH_WIN) | |||||
#if ARCH_WIN | |||||
#include <windows.h> | #include <windows.h> | ||||
#include <direct.h> | #include <direct.h> | ||||
#define mkdir(_dir, _perms) _mkdir(_dir) | #define mkdir(_dir, _perms) _mkdir(_dir) | ||||
@@ -22,14 +27,10 @@ | |||||
#endif | #endif | ||||
#include <dirent.h> | #include <dirent.h> | ||||
#include "plugin.hpp" | |||||
#include "app.hpp" | |||||
#include "asset.hpp" | |||||
#include "util/request.hpp" | |||||
namespace rack { | namespace rack { | ||||
std::list<Plugin*> gPlugins; | std::list<Plugin*> gPlugins; | ||||
std::string gToken; | std::string gToken; | ||||
@@ -52,8 +53,11 @@ void Plugin::addModel(Model *model) { | |||||
models.push_back(model); | models.push_back(model); | ||||
} | } | ||||
//////////////////// | |||||
// private API | |||||
//////////////////// | |||||
static int loadPlugin(std::string path) { | |||||
static bool loadPlugin(std::string path) { | |||||
std::string libraryFilename; | std::string libraryFilename; | ||||
#if ARCH_LIN | #if ARCH_LIN | ||||
libraryFilename = path + "/" + "plugin.so"; | libraryFilename = path + "/" + "plugin.so"; | ||||
@@ -63,6 +67,12 @@ static int loadPlugin(std::string path) { | |||||
libraryFilename = path + "/" + "plugin.dylib"; | libraryFilename = path + "/" + "plugin.dylib"; | ||||
#endif | #endif | ||||
// Check file existence | |||||
if (!systemIsFile(libraryFilename)) { | |||||
warn("Plugin file %s does not exist", libraryFilename.c_str()); | |||||
return false; | |||||
} | |||||
// Load dynamic/shared library | // Load dynamic/shared library | ||||
#if ARCH_WIN | #if ARCH_WIN | ||||
SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); | SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); | ||||
@@ -70,14 +80,14 @@ static int loadPlugin(std::string path) { | |||||
SetErrorMode(0); | SetErrorMode(0); | ||||
if (!handle) { | if (!handle) { | ||||
int error = GetLastError(); | 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); | void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | ||||
if (!handle) { | if (!handle) { | ||||
warn("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); | warn("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); | ||||
return -1; | |||||
return false; | |||||
} | } | ||||
#endif | #endif | ||||
@@ -86,12 +96,12 @@ static int loadPlugin(std::string path) { | |||||
InitCallback initCallback; | InitCallback initCallback; | ||||
#if ARCH_WIN | #if ARCH_WIN | ||||
initCallback = (InitCallback) GetProcAddress(handle, "init"); | initCallback = (InitCallback) GetProcAddress(handle, "init"); | ||||
#elif ARCH_LIN || ARCH_MAC | |||||
#else | |||||
initCallback = (InitCallback) dlsym(handle, "init"); | initCallback = (InitCallback) dlsym(handle, "init"); | ||||
#endif | #endif | ||||
if (!initCallback) { | if (!initCallback) { | ||||
warn("Failed to read init() symbol in %s", libraryFilename.c_str()); | warn("Failed to read init() symbol in %s", libraryFilename.c_str()); | ||||
return -2; | |||||
return false; | |||||
} | } | ||||
// Construct and initialize Plugin instance | // Construct and initialize Plugin instance | ||||
@@ -101,94 +111,19 @@ static int loadPlugin(std::string path) { | |||||
initCallback(plugin); | initCallback(plugin); | ||||
// Reject plugin if slug already exists | // 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 | // Add plugin to list | ||||
gPlugins.push_back(plugin); | gPlugins.push_back(plugin); | ||||
info("Loaded plugin %s", libraryFilename.c_str()); | 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) { | 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"); | json_t *downloadsJ = json_object_get(pluginJ, "downloads"); | ||||
if (downloadsJ) { | if (downloadsJ) { | ||||
#if defined(ARCH_WIN) | |||||
#if ARCH_WIN | |||||
#define DOWNLOADS_ARCH "win" | #define DOWNLOADS_ARCH "win" | ||||
#elif defined(ARCH_MAC) | |||||
#elif ARCH_MAC | |||||
#define DOWNLOADS_ARCH "mac" | #define DOWNLOADS_ARCH "mac" | ||||
#elif defined(ARCH_LIN) | |||||
#elif ARCH_LIN | |||||
#define DOWNLOADS_ARCH "lin" | #define DOWNLOADS_ARCH "lin" | ||||
#endif | #endif | ||||
json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); | 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 = ""; | downloadName = ""; | ||||
return true; | 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) { | bool pluginSync(bool dryRun) { | ||||
if (gToken.empty()) | if (gToken.empty()) | ||||
return false; | return false; | ||||
@@ -378,51 +445,6 @@ bool pluginSync(bool dryRun) { | |||||
return available; | return available; | ||||
} | } | ||||
//////////////////// | |||||
// plugin API | |||||
//////////////////// | |||||
void pluginInit() { | |||||
tagsInit(); | |||||
// TODO | |||||
// If `<local>/plugins/Fundamental` doesn't exist, unzip global Fundamental.zip package into `<local>/plugins` | |||||
// TODO | |||||
// Find all ZIP packages in `<local>/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) { | void pluginLogIn(std::string email, std::string password) { | ||||
json_t *reqJ = json_object(); | json_t *reqJ = json_object(); | ||||
json_object_set(reqJ, "email", json_string(email.c_str())); | json_object_set(reqJ, "email", json_string(email.c_str())); | ||||
@@ -498,6 +520,4 @@ Model *pluginGetModel(std::string pluginSlug, std::string modelSlug) { | |||||
} | } | ||||
} // namespace rack | } // namespace rack |
@@ -1,49 +1,66 @@ | |||||
#include "util/common.hpp" | #include "util/common.hpp" | ||||
#include "asset.hpp" | |||||
#include <stdarg.h> | #include <stdarg.h> | ||||
namespace rack { | 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<std::chrono::milliseconds>(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, ...) { | void debug(const char *format, ...) { | ||||
va_list args; | va_list args; | ||||
va_start(args, format); | va_start(args, format); | ||||
fprintf(gLogFile, "[debug] "); | |||||
vfprintf(gLogFile, format, args); | |||||
fprintf(gLogFile, "\n"); | |||||
fflush(gLogFile); | |||||
printLog("debug", format, args); | |||||
va_end(args); | va_end(args); | ||||
} | } | ||||
void info(const char *format, ...) { | void info(const char *format, ...) { | ||||
va_list args; | va_list args; | ||||
va_start(args, format); | va_start(args, format); | ||||
fprintf(gLogFile, "[info] "); | |||||
vfprintf(gLogFile, format, args); | |||||
fprintf(gLogFile, "\n"); | |||||
fflush(gLogFile); | |||||
printLog("info", format, args); | |||||
va_end(args); | va_end(args); | ||||
} | } | ||||
void warn(const char *format, ...) { | void warn(const char *format, ...) { | ||||
va_list args; | va_list args; | ||||
va_start(args, format); | va_start(args, format); | ||||
fprintf(gLogFile, "[warning] "); | |||||
vfprintf(gLogFile, format, args); | |||||
fprintf(gLogFile, "\n"); | |||||
fflush(gLogFile); | |||||
printLog("warn", format, args); | |||||
va_end(args); | va_end(args); | ||||
} | } | ||||
void fatal(const char *format, ...) { | void fatal(const char *format, ...) { | ||||
va_list args; | va_list args; | ||||
va_start(args, format); | va_start(args, format); | ||||
fprintf(gLogFile, "[fatal] "); | |||||
vfprintf(gLogFile, format, args); | |||||
fprintf(gLogFile, "\n"); | |||||
fflush(gLogFile); | |||||
printLog("fatal", format, args); | |||||
va_end(args); | va_end(args); | ||||
} | } | ||||
@@ -24,43 +24,47 @@ std::string stringf(const char *format, ...) { | |||||
return s; | return s; | ||||
} | } | ||||
std::string lowercase(std::string s) { | |||||
std::string stringLowercase(std::string s) { | |||||
std::transform(s.begin(), s.end(), s.begin(), ::tolower); | std::transform(s.begin(), s.end(), s.begin(), ::tolower); | ||||
return s; | return s; | ||||
} | } | ||||
std::string uppercase(std::string s) { | |||||
std::string stringUppercase(std::string s) { | |||||
std::transform(s.begin(), s.end(), s.begin(), ::toupper); | std::transform(s.begin(), s.end(), s.begin(), ::toupper); | ||||
return s; | return s; | ||||
} | } | ||||
std::string ellipsize(std::string s, size_t len) { | |||||
std::string stringEllipsize(std::string s, size_t len) { | |||||
if (s.size() <= len) | if (s.size() <= len) | ||||
return s; | return s; | ||||
else | else | ||||
return s.substr(0, len - 3) + "..."; | 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; | 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()); | char *pathDup = strdup(path.c_str()); | ||||
std::string directory = dirname(pathDup); | std::string directory = dirname(pathDup); | ||||
free(pathDup); | free(pathDup); | ||||
return directory; | return directory; | ||||
} | } | ||||
std::string extractFilename(std::string path) { | |||||
std::string stringFilename(std::string path) { | |||||
char *pathDup = strdup(path.c_str()); | char *pathDup = strdup(path.c_str()); | ||||
std::string filename = basename(pathDup); | std::string filename = basename(pathDup); | ||||
free(pathDup); | free(pathDup); | ||||
return filename; | 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) | if (!ext) | ||||
return ""; | return ""; | ||||
return ext + 1; | return ext + 1; | ||||
@@ -1,18 +1,81 @@ | |||||
#include "util/common.hpp" | #include "util/common.hpp" | ||||
#include <dirent.h> | |||||
#include <sys/stat.h> | |||||
#if ARCH_WIN | #if ARCH_WIN | ||||
#include <windows.h> | |||||
#include <shellapi.h> | |||||
#include <windows.h> | |||||
#include <shellapi.h> | |||||
#endif | #endif | ||||
namespace rack { | namespace rack { | ||||
void openBrowser(std::string url) { | |||||
std::vector<std::string> systemListEntries(std::string path) { | |||||
std::vector<std::string> 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 | #if ARCH_LIN | ||||
std::string command = "xdg-open " + url; | std::string command = "xdg-open " + url; | ||||
(void)system(command.c_str()); | |||||
(void) system(command.c_str()); | |||||
#endif | #endif | ||||
#if ARCH_MAC | #if ARCH_MAC | ||||
std::string command = "open " + url; | std::string command = "open " + url; | ||||
@@ -439,7 +439,7 @@ void windowRun() { | |||||
windowTitle += gApplicationVersion; | windowTitle += gApplicationVersion; | ||||
if (!gRackWidget->lastPath.empty()) { | if (!gRackWidget->lastPath.empty()) { | ||||
windowTitle += " - "; | windowTitle += " - "; | ||||
windowTitle += extractFilename(gRackWidget->lastPath); | |||||
windowTitle += stringFilename(gRackWidget->lastPath); | |||||
} | } | ||||
if (windowTitle != lastWindowTitle) { | if (windowTitle != lastWindowTitle) { | ||||
glfwSetWindowTitle(gWindow, windowTitle.c_str()); | glfwSetWindowTitle(gWindow, windowTitle.c_str()); | ||||