@@ -2,10 +2,20 @@ | |||
In this document, Mod is Ctrl on Windows/Linux and Cmd on Mac. | |||
### 1.1.5 (in development) | |||
### ??? (in development) | |||
### 1.1.5 (2019-09-29) | |||
- Swap order of tags and brands in Module Browser. | |||
- Disable smoothing for MIDI CC buttons in MIDI-Map. | |||
- Add View > Frame rate menu bar item. | |||
- Hide menu and scrollbars when fullscreen. | |||
- Add key command (F3) for engine CPU meter. | |||
- Add numpad key commands. | |||
- Automatically unzip update on Mac. | |||
- Stop worker threads when engine is paused to save CPU. | |||
- Core | |||
- Disable smoothing for MIDI CC buttons in MIDI-Map. | |||
- Fix sustain pedal release bug when using polyphonic mode in MIDI-CV. | |||
- API | |||
- Add libsamplerate library. | |||
@@ -1,7 +1,7 @@ | |||
{ | |||
"slug": "Core", | |||
"name": "Core", | |||
"version": "1.1.4", | |||
"version": "1.1.5", | |||
"license": "GPL-3.0-only", | |||
"author": "VCV", | |||
"brand": "VCV", | |||
@@ -1,6 +1,6 @@ | |||
RACK_DIR ?= . | |||
VERSION := 1.dev.$(shell git rev-parse --short HEAD) | |||
# VERSION := 1.1.4 | |||
# VERSION := 1.1.5 | |||
FLAGS += -DVERSION=$(VERSION) | |||
FLAGS += -Iinclude -Idep/include | |||
@@ -106,32 +106,32 @@ $(jansson): jansson-2.12 | |||
$(MAKE) -C jansson-2.12 | |||
$(MAKE) -C jansson-2.12 install | |||
openssl-1.1.1b: | |||
$(WGET) "https://www.openssl.org/source/openssl-1.1.1b.tar.gz" | |||
$(SHA256) openssl-1.1.1b.tar.gz 5c557b023230413dfb0756f3137a13e6d726838ccd1430888ad15bfb2b43ea4b | |||
$(UNTAR) openssl-1.1.1b.tar.gz | |||
rm openssl-1.1.1b.tar.gz | |||
openssl-1.1.1d: | |||
$(WGET) "https://www.openssl.org/source/openssl-1.1.1d.tar.gz" | |||
$(SHA256) openssl-1.1.1d.tar.gz 1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 | |||
$(UNTAR) openssl-1.1.1d.tar.gz | |||
rm openssl-1.1.1d.tar.gz | |||
$(openssl): openssl-1.1.1b | |||
$(openssl): openssl-1.1.1d | |||
@# ./config ignores CFLAGS, so hack it in with CC | |||
cd openssl-1.1.1b && CC="$(CC) $(CFLAGS)" ./config --prefix="$(DEP_PATH)" | |||
$(MAKE) -C openssl-1.1.1b | |||
$(MAKE) -C openssl-1.1.1b install_sw | |||
cd openssl-1.1.1d && CC="$(CC) $(CFLAGS)" ./config --prefix="$(DEP_PATH)" | |||
$(MAKE) -C openssl-1.1.1d | |||
$(MAKE) -C openssl-1.1.1d install_sw | |||
curl-7.64.1: | |||
$(WGET) "https://curl.haxx.se/download/curl-7.64.1.tar.gz" | |||
$(SHA256) curl-7.64.1.tar.gz 432d3f466644b9416bc5b649d344116a753aeaa520c8beaf024a90cba9d3d35d | |||
$(UNTAR) curl-7.64.1.tar.gz | |||
rm curl-7.64.1.tar.gz | |||
curl-7.66.0: | |||
$(WGET) "https://curl.haxx.se/download/curl-7.66.0.tar.gz" | |||
$(SHA256) curl-7.66.0.tar.gz d0393da38ac74ffac67313072d7fe75b1fa1010eb5987f63f349b024a36b7ffb | |||
$(UNTAR) curl-7.66.0.tar.gz | |||
rm curl-7.66.0.tar.gz | |||
CURL_FLAGS += --disable-ftp --disable-file --disable-ldap --disable-ldaps --disable-rtsp --disable-proxy --disable-dict --disable-telnet --disable-tftp --disable-pop3 --disable-imap --disable-smb --disable-smtp --disable-gopher --disable-manual --disable-shared --disable-symbol-hiding | |||
CURL_FLAGS += --without-zlib --without-libpsl --without-libmetalink --without-libssh2 --without-librtmp --without-winidn --without-libidn2 --without-nghttp2 --without-brotli | |||
CURL_FLAGS += --with-ssl="$(DEP_PATH)" | |||
$(libcurl): $(openssl) curl-7.64.1 | |||
cd curl-7.64.1 && PKG_CONFIG_PATH= $(CONFIGURE) $(CURL_FLAGS) | |||
$(MAKE) -C curl-7.64.1 | |||
$(MAKE) -C curl-7.64.1 install | |||
$(libcurl): $(openssl) curl-7.66.0 | |||
cd curl-7.66.0 && PKG_CONFIG_PATH= $(CONFIGURE) $(CURL_FLAGS) | |||
$(MAKE) -C curl-7.66.0 | |||
$(MAKE) -C curl-7.66.0 install | |||
libzip-1.5.2: | |||
$(WGET) "https://libzip.org/download/libzip-1.5.2.tar.gz" | |||
@@ -246,7 +246,7 @@ $(pffft): jpommier-pffft-29e4f76ac53b | |||
# Helpers | |||
src: glew-2.1.0 glfw jansson-2.12 speexdsp-SpeexDSP-1.2rc3 openssl-1.1.1b curl-7.64.1 libzip-1.5.2 zlib-1.2.11 rtmidi-4.0.0 rtaudio nanovg nanosvg oui-blendish osdialog jpommier-pffft-29e4f76ac53b | |||
src: glew-2.1.0 glfw jansson-2.12 speexdsp-SpeexDSP-1.2rc3 openssl-1.1.1d curl-7.66.0 libzip-1.5.2 zlib-1.2.11 rtmidi-4.0.0 rtaudio nanovg nanosvg oui-blendish osdialog jpommier-pffft-29e4f76ac53b | |||
clean: | |||
git clean -fdx | |||
@@ -169,6 +169,7 @@ def create_manifest(slug, plugin_dir="."): | |||
manifest['manualUrl'] = input_default("Manual website URL (optional)", manifest.get('manualUrl', "")) | |||
manifest['sourceUrl'] = input_default("Source code URL (optional)", manifest.get('sourceUrl', "")) | |||
manifest['donateUrl'] = input_default("Donate URL (optional)", manifest.get('donateUrl', "")) | |||
manifest['changelogUrl'] = manifest.get('changelogUrl', "") | |||
if 'modules' not in manifest: | |||
manifest['modules'] = [] | |||
@@ -49,7 +49,7 @@ void appDestroy(); | |||
App* appGet(); | |||
/** Accesses the global App pointer */ | |||
#define APP appGet() | |||
#define APP rack::appGet() | |||
} // namespace rack |
@@ -31,6 +31,8 @@ extern std::string pluginsPath; | |||
extern std::string settingsPath; | |||
extern std::string autosavePath; | |||
extern std::string templatePath; | |||
// Only defined on Mac | |||
extern std::string bundlePath; | |||
} // namespace asset | |||
@@ -39,6 +39,174 @@ static const NVGcolor SCHEME_PURPLE = nvgRGB(0xd5, 0x2b, 0xed); | |||
static const NVGcolor SCHEME_LIGHT_GRAY = nvgRGB(0xe6, 0xe6, 0xe6); | |||
static const NVGcolor SCHEME_DARK_GRAY = nvgRGB(0x17, 0x17, 0x17); | |||
//////////////////// | |||
// Lights | |||
//////////////////// | |||
/* | |||
Many of these classes use CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). | |||
To use a red light with its default base class for example, use `RedLight` or `TRedLight<>`. (They are synonymous.) | |||
Use the `TBase` template argument if you want a different base class. | |||
E.g. `RectangleLight<RedLight>` | |||
Although this paradigm might seem confusing at first, it ends up being extremely simple in your plugin code and perfect for "decorating" your classes with appearance traits and behavioral properties. | |||
For example, need a slider with a green LED? Just use | |||
createLightParamCentered<LEDLightSlider<GreenLight>>(...) | |||
*/ | |||
template <typename TBase = app::ModuleLightWidget> | |||
struct TGrayModuleLightWidget : TBase { | |||
TGrayModuleLightWidget() { | |||
this->bgColor = nvgRGB(0x5a, 0x5a, 0x5a); | |||
this->borderColor = nvgRGBA(0, 0, 0, 0x60); | |||
} | |||
}; | |||
typedef TGrayModuleLightWidget<> GrayModuleLightWidget; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TRedLight : TBase { | |||
TRedLight() { | |||
this->addBaseColor(SCHEME_RED); | |||
} | |||
}; | |||
typedef TRedLight<> RedLight; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TGreenLight : TBase { | |||
TGreenLight() { | |||
this->addBaseColor(SCHEME_GREEN); | |||
} | |||
}; | |||
typedef TGreenLight<> GreenLight; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TYellowLight : TBase { | |||
TYellowLight() { | |||
this->addBaseColor(SCHEME_YELLOW); | |||
} | |||
}; | |||
typedef TYellowLight<> YellowLight; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TBlueLight : TBase { | |||
TBlueLight() { | |||
this->addBaseColor(SCHEME_BLUE); | |||
} | |||
}; | |||
typedef TBlueLight<> BlueLight; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TWhiteLight : TBase { | |||
TWhiteLight() { | |||
this->addBaseColor(SCHEME_WHITE); | |||
} | |||
}; | |||
typedef TWhiteLight<> WhiteLight; | |||
/** Reads two adjacent lightIds, so `lightId` and `lightId + 1` must be defined */ | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TGreenRedLight : TBase { | |||
TGreenRedLight() { | |||
this->addBaseColor(SCHEME_GREEN); | |||
this->addBaseColor(SCHEME_RED); | |||
} | |||
}; | |||
typedef TGreenRedLight<> GreenRedLight; | |||
template <typename TBase = GrayModuleLightWidget> | |||
struct TRedGreenBlueLight : TBase { | |||
TRedGreenBlueLight() { | |||
this->addBaseColor(SCHEME_RED); | |||
this->addBaseColor(SCHEME_GREEN); | |||
this->addBaseColor(SCHEME_BLUE); | |||
} | |||
}; | |||
typedef TRedGreenBlueLight<> RedGreenBlueLight; | |||
/** Based on the size of 5mm LEDs */ | |||
template <typename TBase> | |||
struct LargeLight : TBase { | |||
LargeLight() { | |||
this->box.size = app::mm2px(math::Vec(5.179, 5.179)); | |||
} | |||
}; | |||
/** Based on the size of 3mm LEDs */ | |||
template <typename TBase> | |||
struct MediumLight : TBase { | |||
MediumLight() { | |||
this->box.size = app::mm2px(math::Vec(3.176, 3.176)); | |||
} | |||
}; | |||
/** Based on the size of 2mm LEDs */ | |||
template <typename TBase> | |||
struct SmallLight : TBase { | |||
SmallLight() { | |||
this->box.size = app::mm2px(math::Vec(2.176, 2.176)); | |||
} | |||
}; | |||
/** Based on the size of 1mm LEDs */ | |||
template <typename TBase> | |||
struct TinyLight : TBase { | |||
TinyLight() { | |||
this->box.size = app::mm2px(math::Vec(1.088, 1.088)); | |||
} | |||
}; | |||
template <typename TBase> | |||
struct RectangleLight : TBase { | |||
void drawLight(const widget::Widget::DrawArgs& args) override { | |||
nvgBeginPath(args.vg); | |||
nvgRect(args.vg, 0, 0, this->box.size.x, this->box.size.y); | |||
// Background | |||
if (this->bgColor.a > 0.0) { | |||
nvgFillColor(args.vg, this->bgColor); | |||
nvgFill(args.vg); | |||
} | |||
// Foreground | |||
if (this->color.a > 0.0) { | |||
nvgFillColor(args.vg, this->color); | |||
nvgFill(args.vg); | |||
} | |||
// Border | |||
if (this->borderColor.a > 0.0) { | |||
nvgStrokeWidth(args.vg, 0.5); | |||
nvgStrokeColor(args.vg, this->borderColor); | |||
nvgStroke(args.vg); | |||
} | |||
} | |||
}; | |||
/** A light for displaying on top of PB61303. Must add a color by subclassing or templating. */ | |||
template <typename TBase> | |||
struct LEDBezelLight : TBase { | |||
LEDBezelLight() { | |||
this->bgColor = color::BLACK_TRANSPARENT; | |||
this->box.size = app::mm2px(math::Vec(6.0, 6.0)); | |||
} | |||
}; | |||
/** A light to displayed over PB61303. Must add a color by subclassing or templating. | |||
Don't add this as a child of the PB61303 itself. Instead, just place it over it as a sibling in the scene graph, offset by app::mm2px(math::Vec(0.5, 0.5)). | |||
*/ | |||
template <typename TBase> | |||
struct PB61303Light : TBase { | |||
PB61303Light() { | |||
this->bgColor = color::BLACK_TRANSPARENT; | |||
this->box.size = app::mm2px(math::Vec(9.0, 9.0)); | |||
} | |||
}; | |||
//////////////////// | |||
// Knobs | |||
//////////////////// | |||
@@ -390,142 +558,97 @@ struct LEDSliderWhite : LEDSlider { | |||
} | |||
}; | |||
//////////////////// | |||
// Ports | |||
//////////////////// | |||
struct PJ301MPort : app::SvgPort { | |||
PJ301MPort() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/PJ301M.svg"))); | |||
struct LEDSliderHorizontal : app::SvgSlider { | |||
LEDSliderHorizontal() { | |||
horizontal = true; | |||
maxHandlePos = app::mm2px(math::Vec(22.078, 0.738).plus(math::Vec(0, 2))); | |||
minHandlePos = app::mm2px(math::Vec(0.738, 0.738).plus(math::Vec(0, 2))); | |||
setBackgroundSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/LEDSliderHorizontal.svg"))); | |||
} | |||
}; | |||
struct PJ3410Port : app::SvgPort { | |||
PJ3410Port() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/PJ3410.svg"))); | |||
} | |||
}; | |||
struct CL1362Port : app::SvgPort { | |||
CL1362Port() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/CL1362.svg"))); | |||
} | |||
}; | |||
//////////////////// | |||
// Lights | |||
//////////////////// | |||
template <typename TBase, typename TLightBase = RedLight> | |||
struct LightSlider : TBase { | |||
app::ModuleLightWidget* light; | |||
struct GrayModuleLightWidget : app::ModuleLightWidget { | |||
GrayModuleLightWidget() { | |||
bgColor = nvgRGB(0x5a, 0x5a, 0x5a); | |||
borderColor = nvgRGBA(0, 0, 0, 0x60); | |||
LightSlider() { | |||
light = new RectangleLight<TLightBase>; | |||
this->addChild(light); | |||
} | |||
}; | |||
struct RedLight : GrayModuleLightWidget { | |||
RedLight() { | |||
addBaseColor(SCHEME_RED); | |||
void setFirstLightId(int firstLightId) { | |||
if (this->paramQuantity) | |||
light->module = this->paramQuantity->module; | |||
light->firstLightId = firstLightId; | |||
} | |||
}; | |||
struct GreenLight : GrayModuleLightWidget { | |||
GreenLight() { | |||
addBaseColor(SCHEME_GREEN); | |||
void step() override { | |||
TBase::step(); | |||
// Move center of light to center of handle | |||
light->box.pos = this->handle->box.pos | |||
.plus(this->handle->box.size.div(2)) | |||
.minus(light->box.size.div(2)); | |||
} | |||
}; | |||
struct YellowLight : GrayModuleLightWidget { | |||
YellowLight() { | |||
addBaseColor(SCHEME_YELLOW); | |||
template <typename TLightBase = RedLight> | |||
struct LEDLightSlider : LightSlider<LEDSlider, TLightBase> { | |||
LEDLightSlider() { | |||
this->setHandleSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/LEDSliderHandle.svg"))); | |||
this->light->box.size = app::mm2px(math::Vec(1.524, 3.276)); | |||
} | |||
}; | |||
struct BlueLight : GrayModuleLightWidget { | |||
BlueLight() { | |||
addBaseColor(SCHEME_BLUE); | |||
template <typename TLightBase = RedLight> | |||
struct LEDLightSliderHorizontal : LightSlider<LEDSliderHorizontal, TLightBase> { | |||
LEDLightSliderHorizontal() { | |||
this->setHandleSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/LEDSliderHorizontalHandle.svg"))); | |||
this->light->box.size = app::mm2px(math::Vec(3.276, 1.524)); | |||
} | |||
}; | |||
struct WhiteLight : GrayModuleLightWidget { | |||
WhiteLight() { | |||
addBaseColor(SCHEME_WHITE); | |||
} | |||
}; | |||
/** Reads two adjacent lightIds, so `lightId` and `lightId + 1` must be defined */ | |||
struct GreenRedLight : GrayModuleLightWidget { | |||
GreenRedLight() { | |||
addBaseColor(SCHEME_GREEN); | |||
addBaseColor(SCHEME_RED); | |||
} | |||
}; | |||
//////////////////// | |||
// Ports | |||
//////////////////// | |||
struct RedGreenBlueLight : GrayModuleLightWidget { | |||
RedGreenBlueLight() { | |||
addBaseColor(SCHEME_RED); | |||
addBaseColor(SCHEME_GREEN); | |||
addBaseColor(SCHEME_BLUE); | |||
struct PJ301MPort : app::SvgPort { | |||
PJ301MPort() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/PJ301M.svg"))); | |||
} | |||
}; | |||
/** Based on the size of 5mm LEDs */ | |||
template <typename BASE> | |||
struct LargeLight : BASE { | |||
LargeLight() { | |||
this->box.size = app::mm2px(math::Vec(5.179, 5.179)); | |||
struct PJ3410Port : app::SvgPort { | |||
PJ3410Port() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/PJ3410.svg"))); | |||
} | |||
}; | |||
/** Based on the size of 3mm LEDs */ | |||
template <typename BASE> | |||
struct MediumLight : BASE { | |||
MediumLight() { | |||
this->box.size = app::mm2px(math::Vec(3.176, 3.176)); | |||
struct CL1362Port : app::SvgPort { | |||
CL1362Port() { | |||
setSvg(APP->window->loadSvg(asset::system("res/ComponentLibrary/CL1362.svg"))); | |||
} | |||
}; | |||
/** Based on the size of 2mm LEDs */ | |||
template <typename BASE> | |||
struct SmallLight : BASE { | |||
SmallLight() { | |||
this->box.size = app::mm2px(math::Vec(2.176, 2.176)); | |||
} | |||
}; | |||
/** Based on the size of 1mm LEDs */ | |||
template <typename BASE> | |||
struct TinyLight : BASE { | |||
TinyLight() { | |||
this->box.size = app::mm2px(math::Vec(1.088, 1.088)); | |||
} | |||
}; | |||
//////////////////// | |||
// Switches | |||
//////////////////// | |||
/** A light for displaying on top of PB61303. Must add a color by subclassing or templating. */ | |||
template <typename BASE> | |||
struct LEDBezelLight : BASE { | |||
LEDBezelLight() { | |||
this->bgColor = color::BLACK_TRANSPARENT; | |||
this->box.size = app::mm2px(math::Vec(6.0, 6.0)); | |||
template <typename TSwitch> | |||
struct LatchingSwitch : TSwitch { | |||
LatchingSwitch() { | |||
this->momentary = false; | |||
} | |||
}; | |||
/** A light to displayed over PB61303. Must add a color by subclassing or templating. | |||
Don't add this as a child of the PB61303 itself. Instead, just place it over it as a sibling in the scene graph, offset by app::mm2px(math::Vec(0.5, 0.5)). | |||
*/ | |||
template <typename BASE> | |||
struct PB61303Light : BASE { | |||
PB61303Light() { | |||
this->bgColor = color::BLACK_TRANSPARENT; | |||
this->box.size = app::mm2px(math::Vec(9.0, 9.0)); | |||
template <typename TSwitch> | |||
struct MomentarySwitch : TSwitch { | |||
MomentarySwitch() { | |||
this->momentary = true; | |||
} | |||
}; | |||
//////////////////// | |||
// Switches | |||
//////////////////// | |||
struct NKK : app::SvgSwitch { | |||
NKK() { | |||
addFrame(APP->window->loadSvg(asset::system("res/ComponentLibrary/NKK_0.svg"))); | |||
@@ -595,6 +718,24 @@ struct LEDBezel : app::SvgSwitch { | |||
} | |||
}; | |||
template <typename TLightBase = WhiteLight> | |||
struct LEDLightBezel : LEDBezel { | |||
app::ModuleLightWidget* light; | |||
LEDLightBezel() { | |||
light = new LEDBezelLight<TLightBase>; | |||
// Move center of light to center of box | |||
light->box.pos = box.size.div(2).minus(light->box.size.div(2)); | |||
addChild(light); | |||
} | |||
void setFirstLightId(int firstLightId) { | |||
if (paramQuantity) | |||
light->module = paramQuantity->module; | |||
light->firstLightId = firstLightId; | |||
} | |||
}; | |||
struct PB61303 : app::SvgSwitch { | |||
PB61303() { | |||
momentary = true; | |||
@@ -618,6 +759,46 @@ struct ScrewBlack : app::SvgScrew { | |||
} | |||
}; | |||
struct SegmentDisplay : widget::Widget { | |||
int lightsLen = 0; | |||
bool vertical = false; | |||
float margin = app::mm2px(0.5); | |||
void draw(const DrawArgs& args) override { | |||
// Background | |||
nvgBeginPath(args.vg); | |||
nvgRect(args.vg, 0, 0, box.size.x, box.size.y); | |||
nvgFillColor(args.vg, color::BLACK); | |||
nvgFill(args.vg); | |||
Widget::draw(args); | |||
} | |||
template <typename TLightBase = WhiteLight> | |||
void setLights(engine::Module* module, int firstLightId, int lightsLen) { | |||
clearChildren(); | |||
this->lightsLen = lightsLen; | |||
float r = (vertical ? box.size.y : box.size.x) - margin; | |||
for (int i = 0; i < lightsLen; i++) { | |||
float p = float(i) / lightsLen; | |||
app::ModuleLightWidget* light = new RectangleLight<TLightBase>; | |||
if (vertical) { | |||
light->box.pos.y = p * r + margin; | |||
light->box.size.y = r / lightsLen - margin; | |||
light->box.size.x = box.size.x; | |||
} | |||
else { | |||
light->box.pos.x = p * r + margin; | |||
light->box.size.x = r / lightsLen - margin; | |||
light->box.size.y = box.size.y; | |||
} | |||
light->module = module; | |||
light->firstLightId = firstLightId; | |||
firstLightId += light->baseColors.size(); | |||
addChild(light); | |||
} | |||
} | |||
}; | |||
} // namespace componentlibrary | |||
} // namespace rack |
@@ -124,6 +124,21 @@ TModuleLightWidget* createLightCentered(math::Vec pos, engine::Module* module, i | |||
return o; | |||
} | |||
/** Creates a param with a light and calls setFirstLightId() on it. */ | |||
template <class TParamWidget> | |||
TParamWidget* createLightParam(math::Vec pos, engine::Module* module, int paramId, int firstLightId) { | |||
TParamWidget* o = createParam<TParamWidget>(pos, module, paramId); | |||
o->setFirstLightId(firstLightId); | |||
return o; | |||
} | |||
template <class TParamWidget> | |||
TParamWidget* createLightParamCentered(math::Vec pos, engine::Module* module, int paramId, int firstLightId) { | |||
TParamWidget* o = createParamCentered<TParamWidget>(pos, module, paramId); | |||
o->setFirstLightId(firstLightId); | |||
return o; | |||
} | |||
template <class TMenuLabel = ui::MenuLabel> | |||
TMenuLabel * createMenuLabel(std::string text) { | |||
TMenuLabel* o = new TMenuLabel; | |||
@@ -9,10 +9,10 @@ will print something like | |||
[0.123 debug myfile.cpp:45] error: 67 | |||
*/ | |||
#define DEBUG(format, ...) logger::log(rack::logger::DEBUG_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define INFO(format, ...) logger::log(rack::logger::INFO_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define WARN(format, ...) logger::log(rack::logger::WARN_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define FATAL(format, ...) logger::log(rack::logger::FATAL_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define DEBUG(format, ...) rack::logger::log(rack::logger::DEBUG_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define INFO(format, ...) rack::logger::log(rack::logger::INFO_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define WARN(format, ...) rack::logger::log(rack::logger::WARN_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
#define FATAL(format, ...) rack::logger::log(rack::logger::FATAL_LEVEL, __FILE__, __LINE__, format, ##__VA_ARGS__) | |||
namespace rack { | |||
@@ -18,6 +18,7 @@ enum Method { | |||
METHOD_DELETE, | |||
}; | |||
void init(); | |||
/** Requests a JSON API URL over HTTP(S), using the data as the query (GET) or the body (POST, etc) | |||
Caller must json_decref(). | |||
*/ | |||
@@ -100,19 +100,19 @@ | |||
namespace rack { | |||
/** Define this macro before including this header to prevent common namespaces from being included in the main `rack::` namespace. */ | |||
#ifndef RACK_FLATTEN_NAMESPACES | |||
// Import some namespaces for convenience | |||
using namespace logger; | |||
using namespace math; | |||
using namespace widget; | |||
using namespace ui; | |||
using namespace app; | |||
using plugin::Plugin; | |||
using plugin::Model; | |||
using namespace engine; | |||
using namespace componentlibrary; | |||
#endif | |||
// Import some namespaces for convenience | |||
using namespace logger; | |||
using namespace math; | |||
using namespace widget; | |||
using namespace ui; | |||
using namespace app; | |||
using plugin::Plugin; | |||
using plugin::Model; | |||
using namespace engine; | |||
using namespace componentlibrary; | |||
// Import namespace recursively to solve the problem of calling `rack::DEBUG(...)` which expands to `rack::rack::logger(...)`. | |||
namespace rack = rack; | |||
} // namespace rack |
@@ -34,8 +34,7 @@ extern int threadCount; | |||
extern bool paramTooltip; | |||
extern bool cpuMeter; | |||
extern bool lockModules; | |||
extern float frameRateLimit; | |||
extern bool frameRateSync; | |||
extern int frameSwapInterval; | |||
extern float autosavePeriod; | |||
extern bool skipLoadOnLaunch; | |||
extern std::string patchPath; | |||
@@ -5,6 +5,7 @@ | |||
#include <memory> | |||
#include <map> | |||
#define GLEW_STATIC | |||
#define GLEW_NO_GLU | |||
#include <GL/glew.h> | |||
#include <GLFW/glfw3.h> | |||
#include <nanovg.h> | |||
@@ -89,6 +90,7 @@ struct Window { | |||
void setFullScreen(bool fullScreen); | |||
bool isFullScreen(); | |||
bool isFrameOverdue(); | |||
double getMonitorRefreshRate(); | |||
std::shared_ptr<Font> loadFont(const std::string& filename); | |||
std::shared_ptr<Image> loadImage(const std::string& filename); | |||
@@ -0,0 +1,66 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<!-- Created with Inkscape (http://www.inkscape.org/) --> | |||
<svg | |||
xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
xmlns:svg="http://www.w3.org/2000/svg" | |||
xmlns="http://www.w3.org/2000/svg" | |||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
width="1.52414mm" | |||
height="4.1423802mm" | |||
viewBox="0 0 1.52414 4.1423802" | |||
version="1.1" | |||
id="svg164153" | |||
inkscape:version="0.92.4 5da689c313, 2019-01-14" | |||
sodipodi:docname="LEDSliderHandle.svg"> | |||
<defs | |||
id="defs164147" /> | |||
<sodipodi:namedview | |||
id="base" | |||
pagecolor="#ffffff" | |||
bordercolor="#666666" | |||
borderopacity="1.0" | |||
inkscape:pageopacity="0.0" | |||
inkscape:pageshadow="2" | |||
inkscape:zoom="7.9195959" | |||
inkscape:cx="-23.101371" | |||
inkscape:cy="2.1230781" | |||
inkscape:document-units="mm" | |||
inkscape:current-layer="layer1" | |||
showgrid="false" | |||
fit-margin-top="0" | |||
fit-margin-left="0" | |||
fit-margin-right="0" | |||
fit-margin-bottom="0" | |||
inkscape:window-width="1600" | |||
inkscape:window-height="882" | |||
inkscape:window-x="0" | |||
inkscape:window-y="18" | |||
inkscape:window-maximized="0" /> | |||
<metadata | |||
id="metadata164150"> | |||
<rdf:RDF> | |||
<cc:Work | |||
rdf:about=""> | |||
<dc:format>image/svg+xml</dc:format> | |||
<dc:type | |||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
<dc:title></dc:title> | |||
</cc:Work> | |||
</rdf:RDF> | |||
</metadata> | |||
<g | |||
inkscape:label="Layer 1" | |||
inkscape:groupmode="layer" | |||
id="layer1" | |||
transform="translate(-118.96512,-95.791547)"> | |||
<path | |||
style="fill:#5c5c5c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775" | |||
d="m 120.48926,95.791547 v 4.14238 h -1.52414 v -4.14238 z m 0,0" | |||
id="path162650" | |||
inkscape:connector-curvature="0" /> | |||
</g> | |||
</svg> |
@@ -0,0 +1,71 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<!-- Created with Inkscape (http://www.inkscape.org/) --> | |||
<svg | |||
xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
xmlns:svg="http://www.w3.org/2000/svg" | |||
xmlns="http://www.w3.org/2000/svg" | |||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
width="27mm" | |||
height="7mm" | |||
viewBox="0 0 27 7.0000001" | |||
version="1.1" | |||
id="svg163547" | |||
sodipodi:docname="LEDSliderHorizontal.svg" | |||
inkscape:version="0.92.4 5da689c313, 2019-01-14"> | |||
<defs | |||
id="defs163541" /> | |||
<sodipodi:namedview | |||
id="base" | |||
pagecolor="#ffffff" | |||
bordercolor="#666666" | |||
borderopacity="1.0" | |||
inkscape:pageopacity="0.0" | |||
inkscape:pageshadow="2" | |||
inkscape:zoom="1.979899" | |||
inkscape:cx="197.63783" | |||
inkscape:cy="37.645666" | |||
inkscape:document-units="mm" | |||
inkscape:current-layer="layer1" | |||
showgrid="false" | |||
fit-margin-top="0" | |||
fit-margin-left="2" | |||
fit-margin-right="2" | |||
fit-margin-bottom="0" | |||
inkscape:window-width="1600" | |||
inkscape:window-height="882" | |||
inkscape:window-x="0" | |||
inkscape:window-y="18" | |||
inkscape:window-maximized="0" | |||
inkscape:snap-bbox="true" | |||
inkscape:snap-bbox-midpoints="true" | |||
inkscape:snap-page="true" | |||
inkscape:snap-nodes="false" | |||
inkscape:snap-others="false" /> | |||
<metadata | |||
id="metadata163544"> | |||
<rdf:RDF> | |||
<cc:Work | |||
rdf:about=""> | |||
<dc:format>image/svg+xml</dc:format> | |||
<dc:type | |||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
<dc:title /> | |||
</cc:Work> | |||
</rdf:RDF> | |||
</metadata> | |||
<g | |||
inkscape:label="Layer 1" | |||
inkscape:groupmode="layer" | |||
id="layer1" | |||
transform="translate(-105.35713,-115.26781)"> | |||
<path | |||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775" | |||
d="m 132.35708,120.26782 h -26.9999 v -3.00002 h 26.9999 z m 0,0" | |||
id="path159840" | |||
inkscape:connector-curvature="0" /> | |||
</g> | |||
</svg> |
@@ -0,0 +1,66 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<!-- Created with Inkscape (http://www.inkscape.org/) --> | |||
<svg | |||
xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
xmlns:svg="http://www.w3.org/2000/svg" | |||
xmlns="http://www.w3.org/2000/svg" | |||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
width="4.1423802mm" | |||
height="1.52414mm" | |||
viewBox="0 0 4.1423802 1.52414" | |||
version="1.1" | |||
id="svg164153" | |||
inkscape:version="0.92.4 5da689c313, 2019-01-14" | |||
sodipodi:docname="LEDSliderHandleHorizontal.svg"> | |||
<defs | |||
id="defs164147" /> | |||
<sodipodi:namedview | |||
id="base" | |||
pagecolor="#ffffff" | |||
bordercolor="#666666" | |||
borderopacity="1.0" | |||
inkscape:pageopacity="0.0" | |||
inkscape:pageshadow="2" | |||
inkscape:zoom="7.9195959" | |||
inkscape:cx="48.871998" | |||
inkscape:cy="-10.251291" | |||
inkscape:document-units="mm" | |||
inkscape:current-layer="layer1" | |||
showgrid="false" | |||
fit-margin-top="0" | |||
fit-margin-left="0" | |||
fit-margin-right="0" | |||
fit-margin-bottom="0" | |||
inkscape:window-width="1600" | |||
inkscape:window-height="882" | |||
inkscape:window-x="0" | |||
inkscape:window-y="18" | |||
inkscape:window-maximized="0" /> | |||
<metadata | |||
id="metadata164150"> | |||
<rdf:RDF> | |||
<cc:Work | |||
rdf:about=""> | |||
<dc:format>image/svg+xml</dc:format> | |||
<dc:type | |||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
<dc:title></dc:title> | |||
</cc:Work> | |||
</rdf:RDF> | |||
</metadata> | |||
<g | |||
inkscape:label="Layer 1" | |||
inkscape:groupmode="layer" | |||
id="layer1" | |||
transform="translate(-118.96512,-98.409787)"> | |||
<path | |||
style="fill:#5c5c5c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775" | |||
d="m 123.1075,99.933583 h -4.14238 v -1.52414 h 4.14238 z m 0,0" | |||
id="path162650" | |||
inkscape:connector-curvature="0" /> | |||
</g> | |||
</svg> |
@@ -12,7 +12,7 @@ void LightWidget::draw(const DrawArgs& args) { | |||
} | |||
void LightWidget::drawLight(const DrawArgs& args) { | |||
float radius = box.size.x / 2.0; | |||
float radius = std::min(box.size.x, box.size.y) / 2.0; | |||
nvgBeginPath(args.vg); | |||
nvgCircle(args.vg, radius, radius, radius); | |||
@@ -38,7 +38,7 @@ void LightWidget::drawLight(const DrawArgs& args) { | |||
} | |||
void LightWidget::drawHalo(const DrawArgs& args) { | |||
float radius = box.size.x / 2.0; | |||
float radius = std::min(box.size.x, box.size.y) / 2.0; | |||
float oradius = 4.0 * radius; | |||
nvgBeginPath(args.vg); | |||
@@ -327,6 +327,30 @@ struct CursorLockItem : ui::MenuItem { | |||
} | |||
}; | |||
struct FrameRateValueItem : ui::MenuItem { | |||
int frameSwapInterval; | |||
void onAction(const event::Action& e) override { | |||
settings::frameSwapInterval = frameSwapInterval; | |||
} | |||
}; | |||
struct FrameRateItem : ui::MenuItem { | |||
ui::Menu* createChildMenu() override { | |||
ui::Menu* menu = new ui::Menu; | |||
for (int i = 1; i <= 6; i++) { | |||
double frameRate = APP->window->getMonitorRefreshRate() / i; | |||
FrameRateValueItem* item = new FrameRateValueItem; | |||
item->frameSwapInterval = i; | |||
item->text = string::f("%.0lf Hz", frameRate); | |||
item->rightText += CHECKMARK(settings::frameSwapInterval == i); | |||
menu->addChild(item); | |||
} | |||
return menu; | |||
} | |||
}; | |||
struct FullscreenItem : ui::MenuItem { | |||
void onAction(const event::Action& e) override { | |||
APP->window->setFullScreen(!APP->window->isFullScreen()); | |||
@@ -366,6 +390,10 @@ struct ViewButton : MenuButton { | |||
cableTensionSlider->box.size.x = 200.0; | |||
menu->addChild(cableTensionSlider); | |||
FrameRateItem* frameRateItem = new FrameRateItem; | |||
frameRateItem->text = "Frame rate"; | |||
menu->addChild(frameRateItem); | |||
FullscreenItem* fullscreenItem = new FullscreenItem; | |||
fullscreenItem->text = "Fullscreen"; | |||
fullscreenItem->rightText = "F11"; | |||
@@ -477,7 +505,8 @@ struct EngineButton : MenuButton { | |||
CpuMeterItem* cpuMeterItem = new CpuMeterItem; | |||
cpuMeterItem->text = "CPU meter"; | |||
cpuMeterItem->rightText = CHECKMARK(settings::cpuMeter); | |||
cpuMeterItem->rightText = "F3 "; | |||
cpuMeterItem->rightText += CHECKMARK(settings::cpuMeter); | |||
menu->addChild(cpuMeterItem); | |||
SampleRateItem* sampleRateItem = new SampleRateItem; | |||
@@ -219,8 +219,7 @@ struct ModelBox : widget::OpaqueWidget { | |||
void setTooltip(ui::Tooltip* tooltip) { | |||
if (this->tooltip) { | |||
this->tooltip->parent->removeChild(this->tooltip); | |||
delete this->tooltip; | |||
this->tooltip->requestDelete(); | |||
this->tooltip = NULL; | |||
} | |||
@@ -72,6 +72,9 @@ struct ParamTooltip : ui::Tooltip { | |||
Tooltip::step(); | |||
// Position at bottom-right of parameter | |||
box.pos = paramWidget->getAbsoluteOffset(paramWidget->box.size).round(); | |||
// Fit inside parent (copied from Tooltip.cpp) | |||
assert(parent); | |||
box = box.nudge(parent->box.zeroPos()); | |||
} | |||
}; | |||
@@ -77,8 +77,21 @@ void RackScrollWidget::step() { | |||
void RackScrollWidget::draw(const DrawArgs& args) { | |||
// DEBUG("%f %f %f %f", RECT_ARGS(args.clipBox)); | |||
// Hide scrollbars if full screen | |||
bool fullscreen = APP->window->isFullScreen(); | |||
bool horizontalVisible; | |||
bool verticalVisible; | |||
if (fullscreen) { | |||
horizontalVisible = horizontalScrollBar->visible; | |||
verticalVisible = verticalScrollBar->visible; | |||
horizontalScrollBar->visible = false; | |||
verticalScrollBar->visible = false; | |||
} | |||
ScrollWidget::draw(args); | |||
if (fullscreen) { | |||
horizontalScrollBar->visible = horizontalVisible; | |||
verticalScrollBar->visible = verticalVisible; | |||
} | |||
} | |||
void RackScrollWidget::onHoverKey(const event::HoverKey& e) { | |||
@@ -23,7 +23,6 @@ Scene::Scene() { | |||
menuBar = createMenuBar(); | |||
addChild(menuBar); | |||
rackScroll->box.pos.y = menuBar->box.size.y; | |||
moduleBrowser = moduleBrowserCreate(); | |||
moduleBrowser->hide(); | |||
@@ -34,6 +33,10 @@ Scene::~Scene() { | |||
} | |||
void Scene::step() { | |||
bool fullscreen = APP->window->isFullScreen(); | |||
menuBar->visible = !fullscreen; | |||
rackScroll->box.pos.y = menuBar->visible ? menuBar->box.size.y : 0; | |||
// Resize owned descendants | |||
menuBar->box.size.x = box.size.x; | |||
rackScroll->box.size = box.size.minus(rackScroll->box.pos); | |||
@@ -61,96 +64,76 @@ void Scene::onHoverKey(const event::HoverKey& e) { | |||
return; | |||
if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) { | |||
switch (e.key) { | |||
case GLFW_KEY_N: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->resetDialog(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_Q: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->window->close(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_O: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->loadDialog(); | |||
e.consume(this); | |||
} | |||
if ((e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->patch->revertDialog(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_S: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->saveDialog(); | |||
e.consume(this); | |||
} | |||
if ((e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->patch->saveAsDialog(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_Z: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->history->undo(); | |||
e.consume(this); | |||
} | |||
if ((e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->history->redo(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_MINUS: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
float zoom = settings::zoom; | |||
zoom *= 2; | |||
zoom = std::ceil(zoom - 0.01f) - 1; | |||
zoom /= 2; | |||
settings::zoom = zoom; | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_EQUAL: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
float zoom = settings::zoom; | |||
zoom *= 2; | |||
zoom = std::floor(zoom + 0.01f) + 1; | |||
zoom /= 2; | |||
settings::zoom = zoom; | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_0: { | |||
if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
settings::zoom = 0.f; | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_ENTER: { | |||
if ((e.mods & RACK_MOD_MASK) == 0) { | |||
moduleBrowser->show(); | |||
} | |||
e.consume(this); | |||
} break; | |||
case GLFW_KEY_F1: { | |||
if ((e.mods & RACK_MOD_MASK) == 0) { | |||
std::thread t([] { | |||
system::openBrowser("https://vcvrack.com/manual/"); | |||
}); | |||
t.detach(); | |||
e.consume(this); | |||
} | |||
} break; | |||
case GLFW_KEY_F11: { | |||
if ((e.mods & RACK_MOD_MASK) == 0) { | |||
APP->window->setFullScreen(!APP->window->isFullScreen()); | |||
e.consume(this); | |||
} | |||
} break; | |||
if (e.key == GLFW_KEY_N && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->resetDialog(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_Q && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->window->close(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_O && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->loadDialog(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_O && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->patch->revertDialog(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_S && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->patch->saveDialog(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_S && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->patch->saveAsDialog(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_Z && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
APP->history->undo(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_Z && (e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | |||
APP->history->redo(); | |||
e.consume(this); | |||
} | |||
else if ((e.key == GLFW_KEY_MINUS || e.key == GLFW_KEY_KP_SUBTRACT) && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
float zoom = settings::zoom; | |||
zoom *= 2; | |||
zoom = std::ceil(zoom - 0.01f) - 1; | |||
zoom /= 2; | |||
settings::zoom = zoom; | |||
e.consume(this); | |||
} | |||
else if ((e.key == GLFW_KEY_EQUAL || e.key == GLFW_KEY_KP_ADD) && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
float zoom = settings::zoom; | |||
zoom *= 2; | |||
zoom = std::floor(zoom + 0.01f) + 1; | |||
zoom /= 2; | |||
settings::zoom = zoom; | |||
e.consume(this); | |||
} | |||
else if ((e.key == GLFW_KEY_0 || e.key == GLFW_KEY_KP_0) && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | |||
settings::zoom = 0.f; | |||
e.consume(this); | |||
} | |||
else if ((e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER) && (e.mods & RACK_MOD_MASK) == 0) { | |||
moduleBrowser->show(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_F1 && (e.mods & RACK_MOD_MASK) == 0) { | |||
std::thread t([] { | |||
system::openBrowser("https://vcvrack.com/manual/"); | |||
}); | |||
t.detach(); | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_F3 && (e.mods & RACK_MOD_MASK) == 0) { | |||
settings::cpuMeter ^= true; | |||
e.consume(this); | |||
} | |||
else if (e.key == GLFW_KEY_F11 && (e.mods & RACK_MOD_MASK) == 0) { | |||
APP->window->setFullScreen(!APP->window->isFullScreen()); | |||
e.consume(this); | |||
} | |||
} | |||
} | |||
@@ -37,9 +37,19 @@ void init() { | |||
#if defined ARCH_MAC | |||
CFBundleRef bundle = CFBundleGetMainBundle(); | |||
assert(bundle); | |||
CFURLRef bundleUrl = CFBundleCopyBundleURL(bundle); | |||
char bundleBuf[PATH_MAX]; | |||
Boolean success = CFURLGetFileSystemRepresentation(bundleUrl, TRUE, (UInt8*) bundleBuf, sizeof(bundleBuf)); | |||
assert(success); | |||
bundlePath = bundleBuf; | |||
// If the bundle path doesn't end with ".app", assume it's a fake app bundle run from the command line. | |||
if (string::filenameExtension(string::filename(bundlePath)) != "app") | |||
bundlePath = ""; | |||
CFURLRef resourcesUrl = CFBundleCopyResourcesDirectoryURL(bundle); | |||
char resourcesBuf[PATH_MAX]; | |||
Boolean success = CFURLGetFileSystemRepresentation(resourcesUrl, TRUE, (UInt8*) resourcesBuf, sizeof(resourcesBuf)); | |||
success = CFURLGetFileSystemRepresentation(resourcesUrl, TRUE, (UInt8*) resourcesBuf, sizeof(resourcesBuf)); | |||
assert(success); | |||
CFRelease(resourcesUrl); | |||
systemDir = resourcesBuf; | |||
@@ -145,6 +155,7 @@ std::string pluginsPath; | |||
std::string settingsPath; | |||
std::string autosavePath; | |||
std::string templatePath; | |||
std::string bundlePath; | |||
} // namespace asset | |||
@@ -158,9 +158,9 @@ struct MIDI_CV : Module { | |||
// note on | |||
case 0x9: { | |||
if (msg.getValue() > 0) { | |||
int c = (polyMode == MPE_MODE) ? msg.getChannel() : assignChannel(msg.getNote()); | |||
int c = msg.getChannel(); | |||
pressNote(msg.getNote(), &c); | |||
velocities[c] = msg.getValue(); | |||
pressNote(msg.getNote(), c); | |||
} | |||
else { | |||
// For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. | |||
@@ -297,17 +297,24 @@ struct MIDI_CV : Module { | |||
} | |||
} | |||
void pressNote(uint8_t note, int channel) { | |||
void pressNote(uint8_t note, int* channel) { | |||
// 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); | |||
// Determine actual channel | |||
if (polyMode == MPE_MODE) { | |||
// Channel is already decided for us | |||
} | |||
else { | |||
*channel = assignChannel(note); | |||
} | |||
// Set note | |||
notes[channel] = note; | |||
gates[channel] = true; | |||
retriggerPulses[channel].trigger(1e-3); | |||
notes[*channel] = note; | |||
gates[*channel] = true; | |||
retriggerPulses[*channel].trigger(1e-3); | |||
} | |||
void releaseNote(uint8_t note) { | |||
@@ -336,24 +343,15 @@ struct MIDI_CV : Module { | |||
} | |||
void pressPedal() { | |||
if (pedal) | |||
return; | |||
pedal = true; | |||
} | |||
void releasePedal() { | |||
if (!pedal) | |||
return; | |||
pedal = false; | |||
// Clear all gates | |||
for (int c = 0; c < 16; c++) { | |||
gates[c] = false; | |||
} | |||
// Add back only the gates from heldNotes | |||
for (uint8_t note : heldNotes) { | |||
// Find note's channels | |||
for (int c = 0; c < channels; c++) { | |||
if (notes[c] == note) { | |||
gates[c] = true; | |||
} | |||
} | |||
} | |||
// Set last note if monophonic | |||
if (channels == 1) { | |||
if (!heldNotes.empty()) { | |||
@@ -361,6 +359,20 @@ struct MIDI_CV : Module { | |||
notes[0] = lastNote; | |||
} | |||
} | |||
// Clear notes that are not held if polyphonic | |||
else { | |||
for (int c = 0; c < channels; c++) { | |||
if (!gates[c]) | |||
continue; | |||
gates[c] = false; | |||
for (uint8_t note : heldNotes) { | |||
if (notes[c] == note) { | |||
gates[c] = true; | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
void setChannels(int channels) { | |||
@@ -92,7 +92,7 @@ struct MIDI_Map : Module { | |||
Module* module = paramHandles[id].module; | |||
if (!module) | |||
continue; | |||
// Get ParamQuantity | |||
// Get ParamQuantity from ParamHandle | |||
int paramId = paramHandles[id].paramId; | |||
ParamQuantity* paramQuantity = module->paramQuantities[paramId]; | |||
if (!paramQuantity) | |||
@@ -105,7 +105,9 @@ struct MIDI_Map : Module { | |||
filterInitialized[id] = true; | |||
continue; | |||
} | |||
// Set param if value has been initialized | |||
// Check if CC has been set by the MIDI device | |||
if (values[cc] < 0) | |||
continue; | |||
float value = values[cc] / 127.f; | |||
// Detect behavior from MIDI buttons. | |||
if (std::fabs(valueFilters[id].out - value) >= 1.f) { | |||
@@ -1,6 +1,7 @@ | |||
// This source file compiles those annoying implementation-in-header libraries | |||
#define GLEW_STATIC | |||
#define GLEW_NO_GLU | |||
#include <GL/glew.h> | |||
#include <nanovg.h> | |||
@@ -429,12 +429,12 @@ static void Engine_run(Engine* that) { | |||
aheadTime = 0.0; | |||
} | |||
// Launch workers | |||
if (internal->threadCount != settings::threadCount || internal->realTime != settings::realTime) { | |||
Engine_relaunchWorkers(that, settings::threadCount, settings::realTime); | |||
} | |||
if (!internal->paused) { | |||
// Launch workers | |||
if (internal->threadCount != settings::threadCount || internal->realTime != settings::realTime) { | |||
Engine_relaunchWorkers(that, settings::threadCount, settings::realTime); | |||
} | |||
std::lock_guard<std::recursive_mutex> lock(internal->mutex); | |||
// Update expander pointers | |||
@@ -448,6 +448,12 @@ static void Engine_run(Engine* that) { | |||
Engine_step(that); | |||
} | |||
} | |||
else { | |||
// Stop workers while closed | |||
if (internal->threadCount != 1) { | |||
Engine_relaunchWorkers(that, 1, settings::realTime); | |||
} | |||
} | |||
double stepTime = mutexSteps * internal->sampleTime; | |||
aheadTime += stepTime; | |||
@@ -17,6 +17,7 @@ | |||
#include <system.hpp> | |||
#include <string.hpp> | |||
#include <updater.hpp> | |||
#include <network.hpp> | |||
#include <osdialog.h> | |||
#include <thread> | |||
@@ -68,7 +69,7 @@ int main(int argc, char* argv[]) { | |||
// Parse command line arguments | |||
int c; | |||
opterr = 0; | |||
while ((c = getopt(argc, argv, "dhp:s:u:")) != -1) { | |||
while ((c = getopt(argc, argv, "dht:s:u:")) != -1) { | |||
switch (c) { | |||
case 'd': { | |||
settings::devMode = true; | |||
@@ -76,14 +77,11 @@ int main(int argc, char* argv[]) { | |||
case 'h': { | |||
settings::headless = true; | |||
} break; | |||
#if !defined ARCH_MAC | |||
// Due to Mac app translocation and Apple adding a -psn... flag when launched, disable screenshots on Mac for now. | |||
case 'p': { | |||
case 't': { | |||
screenshot = true; | |||
// If parsing number failed, use default value | |||
sscanf(optarg, "%f", &screenshotZoom); | |||
std::sscanf(optarg, "%f", &screenshotZoom); | |||
} break; | |||
#endif | |||
case 's': { | |||
asset::systemDir = optarg; | |||
} break; | |||
@@ -97,6 +95,7 @@ int main(int argc, char* argv[]) { | |||
patchPath = argv[optind]; | |||
} | |||
// Initialize environment | |||
asset::init(); | |||
logger::init(); | |||
@@ -145,6 +144,7 @@ int main(int argc, char* argv[]) { | |||
INFO("Initializing environment"); | |||
random::init(); | |||
network::init(); | |||
midi::init(); | |||
rtmidiInit(); | |||
bridgeInit(); | |||
@@ -30,6 +30,13 @@ static size_t writeStringCallback(char* ptr, size_t size, size_t nmemb, void* us | |||
} | |||
void init() { | |||
// curl_easy_init() calls this automatically, but it's good to make sure this is done on the main thread before other threads are spawned. | |||
// https://curl.haxx.se/libcurl/c/curl_easy_init.html | |||
curl_global_init(CURL_GLOBAL_ALL); | |||
} | |||
json_t* requestJson(Method method, std::string url, json_t* dataJ) { | |||
CURL* curl = createCurl(); | |||
char* reqStr = NULL; | |||
@@ -33,14 +33,16 @@ void PatchManager::init(std::string path) { | |||
return; | |||
} | |||
// To prevent launch crashes, if Rack crashes between now and 15 seconds from now, the "skipAutosaveOnLaunch" property will remain in settings.json, so that in the next launch, the broken autosave will not be loaded. | |||
bool oldSkipLoadOnLaunch = settings::skipLoadOnLaunch; | |||
settings::skipLoadOnLaunch = true; | |||
settings::save(asset::settingsPath); | |||
settings::skipLoadOnLaunch = false; | |||
if (oldSkipLoadOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, possibly caused by a faulty module in your patch. Clear your patch and start over?")) { | |||
this->path = ""; | |||
return; | |||
if (!settings::devMode) { | |||
// To prevent launch crashes, if Rack crashes between now and 15 seconds from now, the "skipAutosaveOnLaunch" property will remain in settings.json, so that in the next launch, the broken autosave will not be loaded. | |||
bool oldSkipLoadOnLaunch = settings::skipLoadOnLaunch; | |||
settings::skipLoadOnLaunch = true; | |||
settings::save(asset::settingsPath); | |||
settings::skipLoadOnLaunch = false; | |||
if (oldSkipLoadOnLaunch && osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, "Rack has recovered from a crash, possibly caused by a faulty module in your patch. Clear your patch and start over?")) { | |||
this->path = ""; | |||
return; | |||
} | |||
} | |||
// Load autosave | |||
@@ -71,7 +71,7 @@ static InitCallback loadLibrary(Plugin* plugin) { | |||
throw UserException(string::f("Failed to load library %s: code %d", libraryFilename.c_str(), error)); | |||
} | |||
#else | |||
void* handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | |||
void* handle = dlopen(libraryFilename.c_str(), RTLD_NOW | RTLD_LOCAL); | |||
if (!handle) { | |||
throw UserException(string::f("Failed to load library %s: %s", libraryFilename.c_str(), dlerror())); | |||
} | |||
@@ -29,8 +29,7 @@ int threadCount = 1; | |||
bool paramTooltip = false; | |||
bool cpuMeter = false; | |||
bool lockModules = false; | |||
float frameRateLimit = 70.0; | |||
bool frameRateSync = true; | |||
int frameSwapInterval = 1; | |||
float autosavePeriod = 15.0; | |||
bool skipLoadOnLaunch = false; | |||
std::string patchPath; | |||
@@ -75,9 +74,7 @@ json_t* toJson() { | |||
json_object_set_new(rootJ, "lockModules", json_boolean(lockModules)); | |||
json_object_set_new(rootJ, "frameRateLimit", json_real(frameRateLimit)); | |||
json_object_set_new(rootJ, "frameRateSync", json_boolean(frameRateSync)); | |||
json_object_set_new(rootJ, "frameSwapInterval", json_integer(frameSwapInterval)); | |||
json_object_set_new(rootJ, "autosavePeriod", json_real(autosavePeriod)); | |||
@@ -160,13 +157,9 @@ void fromJson(json_t* rootJ) { | |||
if (lockModulesJ) | |||
lockModules = json_boolean_value(lockModulesJ); | |||
json_t* frameRateLimitJ = json_object_get(rootJ, "frameRateLimit"); | |||
if (frameRateLimitJ) | |||
frameRateLimit = json_number_value(frameRateLimitJ); | |||
json_t* frameRateSyncJ = json_object_get(rootJ, "frameRateSync"); | |||
if (frameRateSyncJ) | |||
frameRateSync = json_boolean_value(frameRateSyncJ); | |||
json_t* frameSwapIntervalJ = json_object_get(rootJ, "frameSwapInterval"); | |||
if (frameSwapIntervalJ) | |||
frameSwapInterval = json_integer_value(frameSwapIntervalJ); | |||
json_t* autosavePeriodJ = json_object_get(rootJ, "autosavePeriod"); | |||
if (autosavePeriodJ) | |||
@@ -61,24 +61,33 @@ void update() { | |||
if (downloadUrl == "") | |||
return; | |||
#if defined ARCH_WIN || defined ARCH_MAC | |||
// Download update | |||
std::string filename = string::filename(network::urlPath(downloadUrl)); | |||
std::string path = asset::user(filename); | |||
INFO("Download update %s to %s", downloadUrl.c_str(), path.c_str()); | |||
INFO("Downloading update %s to %s", downloadUrl.c_str(), path.c_str()); | |||
network::requestDownload(downloadUrl, path, &progress); | |||
#endif | |||
#if defined ARCH_WIN | |||
// Launch the installer | |||
INFO("Launching update %s", path.c_str()); | |||
system::runProcessDetached(path); | |||
#elif defined ARCH_MAC | |||
// Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | |||
std::string cmd = "open \"" + path + "\""; | |||
std::string cmd; | |||
// std::string appPath = asset::userDir + "/Rack.app"; | |||
// cmd = "rm -rf '" + appPath + "'"; | |||
// std::system(cmd.c_str()); | |||
// // Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | |||
// cmd = "unzip -q '" + path + "' -d '" + asset::userDir + "'"; | |||
// std::system(cmd.c_str()); | |||
// // Open app in Finder | |||
// cmd = "open -R '" + appPath + "'"; | |||
// std::system(cmd.c_str()); | |||
// Open Archive Utility | |||
cmd = "open '" + path + "'"; | |||
std::system(cmd.c_str()); | |||
#else | |||
system::openBrowser(downloadUrl); | |||
#elif defined ARCH_LIN | |||
system::openFolder(asset::user("")); | |||
#endif | |||
APP->window->close(); | |||
@@ -95,6 +95,8 @@ struct Window::Internal { | |||
int lastWindowHeight = 0; | |||
bool ignoreNextMouseDelta = false; | |||
int frameSwapInterval = -1; | |||
double monitorRefreshRate = -1; | |||
}; | |||
@@ -248,8 +250,9 @@ Window::Window() { | |||
glfwSetInputMode(win, GLFW_LOCK_KEY_MODS, 1); | |||
glfwMakeContextCurrent(win); | |||
// Enable v-sync | |||
glfwSwapInterval(settings::frameRateSync ? 1 : 0); | |||
glfwSwapInterval(1); | |||
const GLFWvidmode* monitorMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); | |||
internal->monitorRefreshRate = monitorMode->refreshRate; | |||
// Set window callbacks | |||
glfwSetWindowSizeCallback(win, windowSizeCallback); | |||
@@ -325,7 +328,10 @@ Window::~Window() { | |||
void Window::run() { | |||
frame = 0; | |||
while (!glfwWindowShouldClose(win)) { | |||
frameTimeStart = glfwGetTime(); | |||
double frameTime = glfwGetTime(); | |||
// double frameRate = 1.0 / (frameTime - frameTimeStart); | |||
// DEBUG("%g fps", frameRate); | |||
frameTimeStart = frameTime; | |||
// Make event handlers and step() have a clean nanovg context | |||
nvgReset(vg); | |||
@@ -333,8 +339,14 @@ void Window::run() { | |||
// Poll events | |||
glfwPollEvents(); | |||
// In case glfwPollEvents() set another OpenGL context | |||
// In case glfwPollEvents() sets another OpenGL context | |||
glfwMakeContextCurrent(win); | |||
if (settings::frameSwapInterval != internal->frameSwapInterval) { | |||
internal->frameSwapInterval = settings::frameSwapInterval; | |||
glfwSwapInterval(settings::frameSwapInterval); | |||
} | |||
// Call cursorPosCallback every frame, not just when the mouse moves | |||
{ | |||
double xpos, ypos; | |||
@@ -396,23 +408,10 @@ void Window::run() { | |||
glClearColor(0.0, 0.0, 0.0, 1.0); | |||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||
nvgEndFrame(vg); | |||
glfwSwapBuffers(win); | |||
} | |||
// Limit frame rate | |||
double frameTimeEnd = glfwGetTime(); | |||
if (settings::frameRateLimit > 0.0) { | |||
double frameDuration = frameTimeEnd - frameTimeStart; | |||
double waitDuration = 1.0 / settings::frameRateLimit - frameDuration; | |||
if (waitDuration > 0.0) { | |||
std::this_thread::sleep_for(std::chrono::duration<double>(waitDuration)); | |||
} | |||
} | |||
glfwSwapBuffers(win); | |||
// Compute actual frame rate | |||
frameTimeEnd = glfwGetTime(); | |||
// DEBUG("%g fps", 1 / (endTime - startTime)); | |||
frame++; | |||
} | |||
} | |||
@@ -523,10 +522,15 @@ bool Window::isFullScreen() { | |||
} | |||
bool Window::isFrameOverdue() { | |||
if (settings::frameRateLimit == 0.0) | |||
if (settings::frameSwapInterval == 0) | |||
return false; | |||
double frameDuration = glfwGetTime() - frameTimeStart; | |||
return frameDuration > 1.0 / settings::frameRateLimit; | |||
double frameDeadline = settings::frameSwapInterval / internal->monitorRefreshRate; | |||
return frameDuration > frameDeadline; | |||
} | |||
double Window::getMonitorRefreshRate() { | |||
return internal->monitorRefreshRate; | |||
} | |||
std::shared_ptr<Font> Window::loadFont(const std::string& filename) { | |||