diff --git a/.gitignore b/.gitignore index 8e77f61a..d22540ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /Rack /Rack.exe +/libRack.a /autosave.json /settings.json /plugins diff --git a/.gitmodules b/.gitmodules index 0d52f7cb..1dc85fb4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [submodule "ext/oui-blendish"] path = ext/oui-blendish url = https://github.com/AndrewBelt/oui-blendish.git +[submodule "dep/glfw"] + path = dep/glfw + url = https://github.com/glfw/glfw.git +[submodule "dep/rtaudio"] + path = dep/rtaudio + url = https://github.com/thestk/rtaudio.git diff --git a/CHANGELOG.md b/CHANGELOG.md index d6944c6f..edb6a485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,42 @@ Tip: Use `git checkout v0.4.0` for example to check out any previous version mentioned here. -### dev +### v0.5.1 (2017-12-19) +- Added Plugin Manager support +- Fixed metadata panel in the Add Module window + +- Fundamental + - Added Sequential Switch 1 & 2 + + +### v0.5.0 (2017-11-21) + +- Added zoom scaling from 25% to 200% - Automatically scroll when dragging cables to the edge of the screen +- Added Quad MIDI-to-CV Interface, CC-to-CV, Clock-to-CV, and Trigger-to-CV MIDI interfaces +- Improved support for ASIO, WASAPI, DirectSound, Core Audio, and ALSA audio drivers +- New module browser with search and tags +- Enhanced LED emulation in graphics engine +- File > New attempts to load "template.vcv" in the "Documents/Rack" folder if it exists + +- New Grayscale plugin with Algorhythm, Binary, and Binary² modules - Audible Instruments - - Added Low CPU mode to Braids for draft-quality rendering + - Added extra blend mode functions, alternative modes, and quality settings to Texture Synthesizer + - Added bonus modes and "Disastrous Peace" mode to Resonator + - Added Low CPU mode to Macro Oscillator + - Merged Tidal Modulator and Wavetable Oscillator into a single module + - Fixed Keyframer/Mixer keyframes and channel settings saving +- Fundamental + - Added 8vert, 8-channel attenuverter + - Added Unity, 2-channel mixer + - Changed LED functions in ADSR + + +### v0.4.0 (2017-10-13) -### v0.4.0 - Cables can now stack on output ports - Added sub-menus for each plugin, includes optional plugin metadata like URLs - Added MIDI CC-to-CV Interface, updated MIDI-to-CV Interface @@ -31,7 +58,8 @@ Tip: Use `git checkout v0.4.0` for example to check out any previous version men - Added Keyframer/Mixer -### v0.3.2 +### v0.3.2 (2017-09-25) + - Added key commands - Fixed "invisible knobs/ports" rendering bug for ~2010 Macs - Added "allowCursorLock" to settings.json (set to "false" for touch screen support) @@ -44,7 +72,7 @@ Tip: Use `git checkout v0.4.0` for example to check out any previous version men - Reverted SEQ3 to continuous gates -### v0.3.1 +### v0.3.1 (2017-09-13) - Fixed Windows open dialog current working directory graphics problem - Ctrl/Cmd-C/V to copy/paste from text and password fields @@ -55,5 +83,6 @@ Tip: Use `git checkout v0.4.0` for example to check out any previous version men - tweaks to Fundamental and Audible Instruments plugins -### v0.3.0 +### v0.3.0 (2017-09-10) + - Knobcon public Beta release diff --git a/LICENSE-dist.txt b/LICENSE-dist.txt index 62b28280..7260a68e 100644 --- a/LICENSE-dist.txt +++ b/LICENSE-dist.txt @@ -180,17 +180,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# portaudio +# RtAudio -PortAudio Portable Real-Time Audio Library -Copyright (c) 1999-2011 Ross Bencina and Phil Burk +RtAudio: a set of realtime audio i/o C++ classes +Copyright (c) 2001-2017 Gary P. Scavone -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Any person wishing to distribute modifications to the Software is +asked to send the modifications to the original developer so that +they can be incorporated into the canonical version. This is, +however, not a binding provision of this license. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # RtMidi diff --git a/Makefile b/Makefile index c5b9d45a..22bd908a 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ ifeq ($(ARCH), lin) LDFLAGS += -rdynamic \ -lpthread -lGL -ldl \ $(shell pkg-config --libs gtk+-2.0) \ - -Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lrtaudio -lrtmidi -lossia + -Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl -lossia TARGET = Rack endif @@ -23,7 +23,7 @@ ifeq ($(ARCH), mac) CXXFLAGS += -DAPPLE -stdlib=libc++ LDFLAGS += -stdlib=libc++ -lpthread -ldl \ -framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo \ - -Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lportaudio -lrtmidi -lossia + -Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl -lossia TARGET = Rack BUNDLE = dist/$(TARGET).app endif @@ -33,8 +33,8 @@ ifeq ($(ARCH), win) LDFLAGS += -static-libgcc -static-libstdc++ -lpthread \ -Wl,--export-all-symbols,--out-implib,libRack.a -mwindows \ -lgdi32 -lopengl32 -lcomdlg32 -lole32 \ - -Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lportaudio_x64 -lrtmidi \ - -Wl,-Bstatic -ljansson -lsamplerate -lossia + -Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl \ + -Wl,-Bstatic -ljansson -lspeexdsp -lossia TARGET = Rack.exe OBJECTS = Rack.res endif @@ -58,13 +58,25 @@ ifeq ($(ARCH), win) endif debug: $(TARGET) -ifeq ($(ARCH), mac) - lldb ./Rack -else +ifeq ($(ARCH), lin) LD_LIBRARY_PATH=dep/lib gdb -ex run ./Rack endif +ifeq ($(ARCH), mac) + DYLD_FALLBACK_LIBRARY_PATH=dep/lib gdb -ex run ./Rack +endif +ifeq ($(ARCH), win) + # TODO get rid of the mingw64 path + env PATH=dep/bin:/mingw64/bin gdb -ex run ./Rack +endif + +perf: $(TARGET) +ifeq ($(ARCH), lin) + LD_LIBRARY_PATH=dep/lib perf record --call-graph dwarf ./Rack +endif + clean: + rm -fv libRack.a rm -rfv $(TARGET) build dist # For Windows resources @@ -97,20 +109,22 @@ ifeq ($(ARCH), mac) cp dep/lib/libGLEW.2.1.0.dylib $(BUNDLE)/Contents/MacOS/ cp dep/lib/libglfw.3.dylib $(BUNDLE)/Contents/MacOS/ cp dep/lib/libjansson.4.dylib $(BUNDLE)/Contents/MacOS/ - cp dep/lib/libsamplerate.0.dylib $(BUNDLE)/Contents/MacOS/ + cp dep/lib/libspeexdsp.1.dylib $(BUNDLE)/Contents/MacOS/ cp dep/lib/libcurl.4.dylib $(BUNDLE)/Contents/MacOS/ cp dep/lib/libzip.5.dylib $(BUNDLE)/Contents/MacOS/ - cp dep/lib/libportaudio.2.dylib $(BUNDLE)/Contents/MacOS/ + cp dep/lib/librtaudio.dylib $(BUNDLE)/Contents/MacOS/ cp dep/lib/librtmidi.4.dylib $(BUNDLE)/Contents/MacOS/ + cp dep/lib/libcrypto.1.1.dylib $(BUNDLE)/Contents/MacOS/ install_name_tool -change /usr/local/lib/libGLEW.2.1.0.dylib @executable_path/libGLEW.2.1.0.dylib $(BUNDLE)/Contents/MacOS/Rack install_name_tool -change lib/libglfw.3.dylib @executable_path/libglfw.3.dylib $(BUNDLE)/Contents/MacOS/Rack install_name_tool -change $(PWD)/dep/lib/libjansson.4.dylib @executable_path/libjansson.4.dylib $(BUNDLE)/Contents/MacOS/Rack - install_name_tool -change $(PWD)/dep/lib/libsamplerate.0.dylib @executable_path/libsamplerate.0.dylib $(BUNDLE)/Contents/MacOS/Rack + install_name_tool -change $(PWD)/dep/lib/libspeexdsp.1.dylib @executable_path/libspeexdsp.1.dylib $(BUNDLE)/Contents/MacOS/Rack install_name_tool -change $(PWD)/dep/lib/libcurl.4.dylib @executable_path/libcurl.4.dylib $(BUNDLE)/Contents/MacOS/Rack install_name_tool -change $(PWD)/dep/lib/libzip.5.dylib @executable_path/libzip.5.dylib $(BUNDLE)/Contents/MacOS/Rack - install_name_tool -change $(PWD)/dep/lib/libportaudio.2.dylib @executable_path/libportaudio.2.dylib $(BUNDLE)/Contents/MacOS/Rack + install_name_tool -change librtaudio.dylib @executable_path/librtaudio.dylib $(BUNDLE)/Contents/MacOS/Rack install_name_tool -change $(PWD)/dep/lib/librtmidi.4.dylib @executable_path/librtmidi.4.dylib $(BUNDLE)/Contents/MacOS/Rack + install_name_tool -change $(PWD)/dep/lib/libcrypto.1.1.dylib @executable_path/libcrypto.1.1.dylib $(BUNDLE)/Contents/MacOS/Rack otool -L $(BUNDLE)/Contents/MacOS/Rack @@ -124,6 +138,7 @@ ifeq ($(ARCH), win) mkdir -p dist/Rack cp -R LICENSE* res dist/Rack/ cp Rack.exe dist/Rack/ + strip dist/Rack/Rack.exe cp /mingw64/bin/libwinpthread-1.dll dist/Rack/ cp /mingw64/bin/zlib1.dll dist/Rack/ cp /mingw64/bin/libstdc++-6.dll dist/Rack/ @@ -133,9 +148,11 @@ ifeq ($(ARCH), win) cp dep/bin/libcurl-4.dll dist/Rack/ cp dep/bin/libjansson-4.dll dist/Rack/ cp dep/bin/librtmidi-4.dll dist/Rack/ - cp dep/bin/libsamplerate-0.dll dist/Rack/ + cp dep/bin/libspeexdsp-1.dll dist/Rack/ cp dep/bin/libzip-5.dll dist/Rack/ - cp dep/bin/portaudio_x64.dll dist/Rack/ + cp dep/bin/librtaudio.dll dist/Rack/ + cp dep/bin/libcrypto-1_1-x64.dll dist/Rack/ + cp dep/bin/libssl-1_1-x64.dll dist/Rack/ mkdir -p dist/Rack/plugins cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ # Make ZIP @@ -148,20 +165,37 @@ ifeq ($(ARCH), lin) mkdir -p dist/Rack cp -R LICENSE* res dist/Rack/ cp Rack Rack.sh dist/Rack/ - cp dep/lib/libsamplerate.so.0 dist/Rack/ + cp dep/lib/libspeexdsp.so dist/Rack/ cp dep/lib/libjansson.so.4 dist/Rack/ cp dep/lib/libGLEW.so.2.1 dist/Rack/ cp dep/lib/libglfw.so.3 dist/Rack/ cp dep/lib/libcurl.so.4 dist/Rack/ cp dep/lib/libzip.so.5 dist/Rack/ - cp dep/lib/librtaudio.so.6 dist/Rack/ + cp dep/lib/librtaudio.so dist/Rack/ cp dep/lib/librtmidi.so.4 dist/Rack/ - cp /usr/local/lib/libossia.so dist/Rack/ + cp dep/lib/libssl.so.1.1 dist/Rack/ + cp dep/lib/libcrypto.so.1.1 dist/Rack/ mkdir -p dist/Rack/plugins - # Make ZIP cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ - cp -R plugins/Tutorial/dist/Template dist/Rack/plugins/ + # Make ZIP + cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack +endif + + +# Obviously this will only work if you have the private keys to my server +UPLOAD_URL = vortico@vcvrack.com:files/ +upload: dist distplugins +ifeq ($(ARCH), mac) + rsync dist/*.dmg $(UPLOAD_URL) -zP +endif +ifeq ($(ARCH), win) + rsync dist/*.exe $(UPLOAD_URL) -P + rsync dist/*.zip $(UPLOAD_URL) -P +endif +ifeq ($(ARCH), lin) + rsync dist/*.zip $(UPLOAD_URL) -zP endif + rsync plugins/*/dist/*.zip $(UPLOAD_URL) -zP # Plugin helpers diff --git a/README.md b/README.md index ddcaaf42..6f9d0e72 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ You may use make's `-j$(nproc)` flag to parallelize builds across all your CPU c make dep +You may use `make dep RTAUDIO_ALL_APIS=1` to attempt to build with all audio driver APIs enabled for your operating system. + You should see a message that all dependencies built successfully. Build Rack. @@ -81,6 +83,8 @@ Build plugin. ## License -Rack source code by [Andrew Belt](https://andrewbelt.name/) licensed under [BSD-3-Clause](LICENSE.txt) +Source code licensed under [BSD-3-Clause](LICENSE.txt) by [Andrew Belt](https://andrewbelt.name/) + +Component Library graphics in `res/ComponentLibrary` licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) by [Grayscale](http://grayscale.info/) -Component Library graphics by [Grayscale](http://grayscale.info/) licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) +VCV logo is © 2017 diff --git a/compile.mk b/compile.mk index 4ee00adf..a620a636 100644 --- a/compile.mk +++ b/compile.mk @@ -1,5 +1,5 @@ ifdef VERSION -FLAGS += -DVERSION=$(VERSION) + FLAGS += -DVERSION=$(VERSION) endif # Generate dependency files alongside the object files @@ -9,7 +9,7 @@ FLAGS += -g FLAGS += -O3 -march=nocona -ffast-math -fno-finite-math-only FLAGS += -Wall -Wextra -Wno-unused-parameter ifneq ($(ARCH), mac) -CXXFLAGS += -Wsuggest-override + CXXFLAGS += -Wsuggest-override endif CXXFLAGS += -std=c++14 @@ -32,6 +32,9 @@ ifeq ($(ARCH), win) FLAGS += -D_USE_MATH_DEFINES endif +CFLAGS += $(FLAGS) +CXXFLAGS += $(FLAGS) + OBJECTS += $(patsubst %, build/%.o, $(SOURCES)) DEPS = $(patsubst %, build/%.d, $(SOURCES)) @@ -47,16 +50,16 @@ $(TARGET): $(OBJECTS) build/%.c.o: %.c @mkdir -p $(@D) - $(CC) $(FLAGS) $(CFLAGS) -c -o $@ $< + $(CC) $(CFLAGS) -c -o $@ $< build/%.cpp.o: %.cpp @mkdir -p $(@D) - $(CXX) $(FLAGS) $(CXXFLAGS) -c -o $@ $< + $(CXX) $(CXXFLAGS) -c -o $@ $< build/%.cc.o: %.cc @mkdir -p $(@D) - $(CXX) $(FLAGS) $(CXXFLAGS) -c -o $@ $< + $(CXX) $(CXXFLAGS) -c -o $@ $< build/%.m.o: %.m @mkdir -p $(@D) - $(CC) $(FLAGS) $(CFLAGS) -c -o $@ $< + $(CC) $(CFLAGS) -c -o $@ $< diff --git a/dep/Makefile b/dep/Makefile index e414a00f..394a07d1 100755 --- a/dep/Makefile +++ b/dep/Makefile @@ -26,11 +26,12 @@ ifeq ($(ARCH),lin) glew = lib/libGLEW.so glfw = lib/libglfw.so jansson = lib/libjansson.so - libsamplerate = lib/libsamplerate.so + libspeexdsp = lib/libspeexdsp.so libcurl = lib/libcurl.so libzip = lib/libzip.so rtmidi = lib/librtmidi.so rtaudio = lib/librtaudio.so + openssl = lib/libssl.so ossia = lib/libossia.so endif @@ -38,10 +39,12 @@ ifeq ($(ARCH),mac) glew = lib/libGLEW.dylib glfw = lib/libglfw.dylib jansson = lib/libjansson.dylib - libsamplerate = lib/libsamplerate.dylib + libspeexdsp = lib/libspeexdsp.dylib libcurl = lib/libcurl.dylib libzip = lib/libzip.dylib rtmidi = lib/librtmidi.dylib + rtaudio = lib/librtaudio.dylib + openssl = lib/libssl.dylib ossia = lib/libossia.so endif @@ -49,17 +52,31 @@ ifeq ($(ARCH),win) glew = bin/glew32.dll glfw = bin/glfw3.dll jansson = bin/libjansson-4.dll - libsamplerate = bin/libsamplerate-0.dll + libspeexdsp = bin/libspeexdsp.dll libcurl = bin/libcurl-4.dll libzip = bin/libzip-5.dll rtmidi = bin/librtmidi-4.dll - ossia = lib/libossia.dll + rtaudio = bin/librtaudio.dll + openssl = bin/libssl-1_1-x64.dll +endif + +# Library configuration +ifdef RTAUDIO_ALL_APIS +ifeq ($(ARCH),mac) + RTAUDIO_FLAGS = -DAUDIO_OSX_CORE=ON +endif +ifeq ($(ARCH),win) + RTAUDIO_FLAGS = -DAUDIO_WINDOWS_DS=ON -DAUDIO_WINDOWS_WASAPI=ON -DAUDIO_WINDOWS_ASIO=ON +endif +ifeq ($(ARCH),lin) + RTAUDIO_FLAGS = -DAUDIO_LINUX_ALSA=ON +endif endif .NOTPARALLEL: -all: $(glew) $(glfw) $(jansson) $(libsamplerate) $(libcurl) $(libzip) $(rtmidi) $(rtaudio) $(ossia) +all: $(glew) $(glfw) $(jansson) $(libspeexdsp) $(libcurl) $(libzip) $(rtmidi) $(rtaudio) @echo "" @echo "#######################################" @echo "# Built all dependencies successfully #" @@ -74,17 +91,11 @@ $(glew): $(MAKE) -C glew-2.1.0 GLEW_DEST="$(LOCAL)" LIBDIR="$(LOCAL)/lib" install $(glfw): - $(WGET) https://github.com/glfw/glfw/releases/download/3.2.1/glfw-3.2.1.zip - $(UNZIP) glfw-3.2.1.zip - cd glfw-3.2.1 && $(CMAKE) . \ + cd glfw && $(CMAKE) . \ -DCMAKE_INSTALL_PREFIX="$(LOCAL)" -DBUILD_SHARED_LIBS=ON \ - -DGLFW_USE_CHDIR=OFF -DGLFW_USE_MENUBAR=ON -DGLFW_USE_RETINA=ON - $(MAKE) -C glfw-3.2.1 - $(MAKE) -C glfw-3.2.1 install -ifeq ($(ARCH),win) - # Not sure why the GLFW build system puts a .dll in the lib directory - mv "$(LOCAL)/lib/glfw3.dll" "$(LOCAL)/bin/" -endif + -DGLFW_COCOA_CHDIR_RESOURCES=OFF -DGLFW_COCOA_MENUBAR=ON -DGLFW_COCOA_RETINA_FRAMEBUFFER=ON + $(MAKE) -C glfw + $(MAKE) -C glfw install $(jansson): $(WGET) http://www.digip.org/jansson/releases/jansson-2.10.tar.gz @@ -93,19 +104,27 @@ $(jansson): $(MAKE) -C jansson-2.10 $(MAKE) -C jansson-2.10 install -$(libsamplerate): - $(WGET) http://www.mega-nerd.com/SRC/libsamplerate-0.1.9.tar.gz - $(UNTAR) libsamplerate-0.1.9.tar.gz - cd libsamplerate-0.1.9 && ./configure --prefix="$(LOCAL)" --disable-fftw --disable-sndfile - $(MAKE) -C libsamplerate-0.1.9/src - $(MAKE) -C libsamplerate-0.1.9/src install - -$(libcurl): +$(libspeexdsp): + $(WGET) https://github.com/xiph/speexdsp/archive/SpeexDSP-1.2rc3.tar.gz + $(UNTAR) SpeexDSP-1.2rc3.tar.gz + cd speexdsp-SpeexDSP-1.2rc3 && ./autogen.sh && ./configure --prefix="$(LOCAL)" + $(MAKE) -C speexdsp-SpeexDSP-1.2rc3 + $(MAKE) -C speexdsp-SpeexDSP-1.2rc3 install + +$(openssl): + $(WGET) https://www.openssl.org/source/openssl-1.1.0g.tar.gz + $(UNTAR) openssl-1.1.0g.tar.gz + cd openssl-1.1.0g && ./config --prefix="$(LOCAL)" + $(MAKE) -C openssl-1.1.0g + $(MAKE) -C openssl-1.1.0g install + +$(libcurl): $(openssl) $(WGET) https://github.com/curl/curl/releases/download/curl-7_56_0/curl-7.56.0.tar.gz $(UNTAR) curl-7.56.0.tar.gz cd curl-7.56.0 && ./configure --prefix="$(LOCAL)" \ --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 \ - --without-zlib --without-ssl --without-ca-bundle --without-ca-path --without-ca-fallback --without-libpsl --without-libmetalink --without-libssh2 --without-librtmp --without-winidn --without-libidn2 --without-nghttp2 + --without-zlib --without-libpsl --without-libmetalink --without-libssh2 --without-librtmp --without-winidn --without-libidn2 --without-nghttp2 \ + --without-ca-bundle --with-ca-fallback --with-ssl="$(LOCAL)" $(MAKE) -C curl-7.56.0 $(MAKE) -C curl-7.56.0 install @@ -117,19 +136,16 @@ $(libzip): $(MAKE) -C libzip-1.2.0 install $(rtmidi): - $(WGET) http://www.music.mcgill.ca/~gary/rtmidi/release/rtmidi-3.0.0.tar.gz - $(UNTAR) rtmidi-3.0.0.tar.gz - cd rtmidi-3.0.0 && ./configure --prefix="$(LOCAL)" - $(MAKE) -C rtmidi-3.0.0 - $(MAKE) -C rtmidi-3.0.0 install + git clone https://github.com/thestk/rtmidi.git + cd rtmidi && ./autogen.sh --no-configure && ./configure --prefix="$(LOCAL)" + $(MAKE) -C rtmidi + $(MAKE) -C rtmidi install $(rtaudio): - $(WGET) http://www.music.mcgill.ca/~gary/rtaudio/release/rtaudio-5.0.0.tar.gz - $(UNTAR) rtaudio-5.0.0.tar.gz - cd rtaudio-5.0.0 && ./configure --prefix="$(LOCAL)" - $(MAKE) -C rtaudio-5.0.0 - $(MAKE) -C rtaudio-5.0.0 install - + cd rtaudio && mkdir -p cmakebuild + cd rtaudio/cmakebuild && cmake -G 'Unix Makefiles' -DCMAKE_INSTALL_PREFIX="$(LOCAL)" $(RTAUDIO_FLAGS) .. + $(MAKE) -C rtaudio/cmakebuild + $(MAKE) -C rtaudio/cmakebuild install $(ossia): # TODO use release tarball instead of building it locally git clone https://github.com/OSSIA/libossia --depth=1 @@ -141,4 +157,4 @@ $(ossia): $(MAKE) install clean: - git clean -ffdxi + git clean -ffdx diff --git a/dep/glfw b/dep/glfw new file mode 160000 index 00000000..682f1cf2 --- /dev/null +++ b/dep/glfw @@ -0,0 +1 @@ +Subproject commit 682f1cf203707f21c2eed4fa3f89c23c52accc49 diff --git a/dep/rtaudio b/dep/rtaudio new file mode 160000 index 00000000..ce13dfbf --- /dev/null +++ b/dep/rtaudio @@ -0,0 +1 @@ +Subproject commit ce13dfbf30fd1ab4e7f7eff8886a80f144c75e5d diff --git a/include/app.hpp b/include/app.hpp index 5acb6bef..bad646d0 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -20,10 +20,9 @@ struct SVGPanel; // module //////////////////// -// A 1U module should be 15x380. Thus the width of a module should be a factor of 15. +// A 1HPx3U module should be 15x380. Thus the width of a module should be a factor of 15. #define RACK_GRID_WIDTH 15 #define RACK_GRID_HEIGHT 380 - static const Vec RACK_GRID_SIZE = Vec(15, 380); @@ -48,6 +47,8 @@ struct ModuleWidget : OpaqueWidget { virtual json_t *toJson(); virtual void fromJson(json_t *rootJ); + virtual void create(); + virtual void _delete(); /** Disconnects cables from all ports Called when the user clicks Disconnect Cables in the context menu. */ @@ -75,14 +76,11 @@ struct ModuleWidget : OpaqueWidget { void onDragMove(EventDragMove &e) override; }; -struct ValueLight; struct WireWidget : OpaqueWidget { Port *outputPort = NULL; Port *inputPort = NULL; Port *hoveredOutputPort = NULL; Port *hoveredInputPort = NULL; - ValueLight *inputLight; - ValueLight *outputLight; Wire *wire = NULL; NVGcolor color; @@ -199,6 +197,8 @@ struct ParamWidget : OpaqueWidget, QuantityWidget { struct Knob : ParamWidget { /** Snap to nearest integer while dragging */ bool snap = false; + /** Multiplier for mouse movement to adjust knob value */ + float speed = 1.0; float dragValue; void onDragStart(EventDragStart &e) override; void onDragMove(EventDragMove &e) override; @@ -226,14 +226,14 @@ struct SVGKnob : virtual Knob, FramebufferWidget { void onChange(EventChange &e) override; }; -struct SVGSlider : Knob, FramebufferWidget { +struct SVGFader : Knob, FramebufferWidget { /** Intermediate positions will be interpolated between these positions */ Vec minHandlePos, maxHandlePos; /** Not owned */ SVGWidget *background; SVGWidget *handle; - SVGSlider(); + SVGFader(); void step() override; void onChange(EventChange &e) override; }; @@ -249,7 +249,6 @@ struct SVGSwitch : virtual Switch, FramebufferWidget { SVGSwitch(); /** Adds an SVG file to represent the next switch position */ void addFrame(std::shared_ptr svg); - void step() override; void onChange(EventChange &e) override; }; @@ -271,6 +270,8 @@ struct MomentarySwitch : virtual Switch { void randomize() override {} void onDragStart(EventDragStart &e) override { setValue(maxValue); + EventAction eAction; + onAction(eAction); } void onDragEnd(EventDragEnd &e) override { setValue(minValue); @@ -285,6 +286,8 @@ struct LightWidget : TransparentWidget { NVGcolor bgColor = nvgRGBf(0, 0, 0); NVGcolor color = nvgRGBf(1, 1, 1); void draw(NVGcontext *vg) override; + virtual void drawLight(NVGcontext *vg); + virtual void drawHalo(NVGcontext *vg); }; /** Mixes a list of colors based on a list of brightness values */ diff --git a/include/components.hpp b/include/components.hpp index c300e44b..cac1cf91 100644 --- a/include/components.hpp +++ b/include/components.hpp @@ -322,7 +322,7 @@ struct BefacoTinyKnob : SVGKnob { } }; -struct BefacoSlidePot : SVGSlider { +struct BefacoSlidePot : SVGFader { BefacoSlidePot() { Vec margin = Vec(3.5, 3.5); maxHandlePos = Vec(-1, -2).plus(margin); @@ -461,6 +461,14 @@ struct CKSS : SVGSwitch, ToggleSwitch { } }; +struct CKSSThree : SVGSwitch, ToggleSwitch { + CKSSThree() { + addFrame(SVG::load(assetGlobal("res/ComponentLibrary/CKSSThree_0.svg"))); + addFrame(SVG::load(assetGlobal("res/ComponentLibrary/CKSSThree_1.svg"))); + addFrame(SVG::load(assetGlobal("res/ComponentLibrary/CKSSThree_2.svg"))); + } +}; + struct CKD6 : SVGSwitch, MomentarySwitch { CKD6() { addFrame(SVG::load(assetGlobal("res/ComponentLibrary/CKD6_0.svg"))); diff --git a/include/dsp/filter.hpp b/include/dsp/filter.hpp index 35098ecf..a29785bd 100644 --- a/include/dsp/filter.hpp +++ b/include/dsp/filter.hpp @@ -51,6 +51,11 @@ struct SlewLimiter { float rise = 1.0; float fall = 1.0; float out = 0.0; + + void setRiseFall(float _rise, float _fall) { + rise = _rise; + fall = _fall; + } float process(float in) { float delta = clampf(in - out, -fall, rise); out += delta; diff --git a/include/dsp/samplerate.hpp b/include/dsp/samplerate.hpp index 7cc988cf..a4a99880 100644 --- a/include/dsp/samplerate.hpp +++ b/include/dsp/samplerate.hpp @@ -1,7 +1,8 @@ #pragma once #include -#include +#include +#include #include "frame.hpp" @@ -9,41 +10,46 @@ namespace rack { template struct SampleRateConverter { - SRC_STATE *state; - SRC_DATA data; + SpeexResamplerState *state = NULL; + bool bypass = false; SampleRateConverter() { int error; - state = src_new(SRC_SINC_FASTEST, CHANNELS, &error); - assert(!error); - - data.src_ratio = 1.0; - data.end_of_input = false; + state = speex_resampler_init(CHANNELS, 44100, 44100, SPEEX_RESAMPLER_QUALITY_DEFAULT, &error); + assert(error == RESAMPLER_ERR_SUCCESS); } ~SampleRateConverter() { - src_delete(state); + speex_resampler_destroy(state); } - /** output_sample_rate / input_sample_rate */ - void setRatio(float r) { - src_set_ratio(state, r); - data.src_ratio = r; + + void setQuality(int quality) { + speex_resampler_set_quality(state, quality); } - void setRatioSmooth(float r) { - data.src_ratio = r; + + void setRates(int inRate, int outRate) { + spx_uint32_t oldInRate, oldOutRate; + speex_resampler_get_rate(state, &oldInRate, &oldOutRate); + if (inRate == (int) oldInRate && outRate == (int) oldOutRate) + return; + int error = speex_resampler_set_rate(state, inRate, outRate); + assert(error == RESAMPLER_ERR_SUCCESS); } + /** `in` and `out` are interlaced with the number of channels */ void process(const Frame *in, int *inFrames, Frame *out, int *outFrames) { - // Old versions of libsamplerate use float* here instead of const float* - data.data_in = (float*) in; - data.input_frames = *inFrames; - data.data_out = (float*) out; - data.output_frames = *outFrames; - src_process(state, &data); - *inFrames = data.input_frames_used; - *outFrames = data.output_frames_gen; + if (bypass) { + int len = std::min(*inFrames, *outFrames); + memcpy(out, in, len * sizeof(Frame)); + *inFrames = len; + *outFrames = len; + return; + } + speex_resampler_process_interleaved_float(state, (const float*)in, (unsigned int*)inFrames, (float*)out, (unsigned int*)outFrames); } + void reset() { - src_reset(state); + int error = speex_resampler_reset_mem(state); + assert(error == RESAMPLER_ERR_SUCCESS); } }; diff --git a/include/dsp/vumeter.hpp b/include/dsp/vumeter.hpp new file mode 100644 index 00000000..73792c81 --- /dev/null +++ b/include/dsp/vumeter.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "math.hpp" + + +namespace rack { + + +struct VUMeter { + /** Decibel level difference between adjacent meter lights */ + float dBInterval = 3.0; + float dBScaled; + /** Value should be scaled so that 1.0 is clipping */ + void setValue(float v) { + dBScaled = log10f(fabsf(v)) * 20.0 / dBInterval; + } + /** Returns the brightness of the light indexed by i + Light 0 is a clip light (red) which is either on or off. + All others are smooth lights which are fully bright at -dBInterval*i and higher, and fully off at -dBInterval*(i-1). + */ + float getBrightness(int i) { + if (i == 0) { + return (dBScaled >= 0.0) ? 1.0 : 0.0; + } + else { + return clampf(dBScaled + i, 0.0, 1.0); + } + } +}; + + +} // namespace rack diff --git a/include/engine.hpp b/include/engine.hpp index e5883a51..a387105a 100644 --- a/include/engine.hpp +++ b/include/engine.hpp @@ -21,7 +21,7 @@ struct Light { float value = 0.0; float getBrightness(); void setBrightness(float brightness) { - value = brightness * brightness; + value = (brightness > 0.f) ? brightness * brightness : 0.f; } void setBrightnessSmooth(float brightness); }; @@ -73,11 +73,14 @@ struct Module { virtual void step() {} virtual void onSampleRateChange() {} - /** Override these to implement spacial behavior when user clicks Initialize and Randomize */ - virtual void reset() {} - virtual void randomize() {} - /** Deprecated */ - virtual void initialize() final {} + /** Called when module is created by the Add Module popup, cloning, or when loading a patch or autosave */ + virtual void onCreate() {} + /** Called when user explicitly deletes the module, not when Rack is closed or a new patch is loaded */ + virtual void onDelete() {} + /** Called when user clicks Initialize in the module context menu */ + virtual void onReset() {} + /** Called when user clicks Randomize in the module context menu */ + virtual void onRandomize() {} /** Override these to store extra internal data in the "data" property */ virtual json_t *toJson() { return NULL; } diff --git a/include/gui.hpp b/include/gui.hpp index 9df01144..fd6c0e77 100644 --- a/include/gui.hpp +++ b/include/gui.hpp @@ -17,8 +17,14 @@ namespace rack { extern GLFWwindow *gWindow; extern NVGcontext *gVg; extern NVGcontext *gFramebufferVg; +/** The default font to use for GUI elements */ extern std::shared_ptr gGuiFont; +/** The scaling ratio */ extern float gPixelRatio; +/* The ratio between the framebuffer size and the window size reported by the OS. +This is not equal to gPixelRatio in general. +*/ +extern float gWindowRatio; extern bool gAllowCursorLock; extern int gGuiFrame; extern Vec gMousePos; diff --git a/include/math.hpp b/include/math.hpp index 0b94c9c1..815f3746 100644 --- a/include/math.hpp +++ b/include/math.hpp @@ -120,15 +120,6 @@ inline float sincf(float x) { return sinf(x) / x; } -inline float getf(const float *p, float v = 0.0) { - return p ? *p : v; -} - -inline void setf(float *p, float v) { - if (p) - *p = v; -} - /** Linearly interpolate an array `p` with index `x` Assumes that the array at `p` is of length at least floor(x)+1. */ @@ -139,7 +130,7 @@ inline float interpf(const float *p, float x) { } /** Complex multiply c = a * b -It is of course acceptable to reuse arguments +Arguments may be the same pointers i.e. cmultf(&ar, &ai, ar, ai, br, bi) */ inline void cmultf(float *cr, float *ci, float ar, float ai, float br, float bi) { diff --git a/include/plugin.hpp b/include/plugin.hpp index e8802456..c8fb98cd 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -1,60 +1,13 @@ #pragma once #include #include +#include "tags.hpp" #include namespace rack { -enum ModelTag { - AMPLIFIER_TAG, - ATTENUATOR_TAG, - BLANK_TAG, - CLOCK_TAG, - CONTROLLER_TAG, - DELAY_TAG, - DIGITAL_TAG, - DISTORTION_TAG, - DRUM_TAG, - DUAL_TAG, - DYNAMICS_TAG, - EFFECT_TAG, - ENVELOPE_FOLLOWER_TAG, - ENVELOPE_GENERATOR_TAG, - EQUALIZER_TAG, - EXTERNAL_TAG, - FILTER_TAG, - FUNCTION_GENERATOR_TAG, - GRANULAR_TAG, - LFO_TAG, - LOGIC_TAG, - LOW_PASS_GATE_TAG, - MIDI_TAG, - MIXER_TAG, - MULTIPLE_TAG, - NOISE_TAG, - OSCILLATOR_TAG, - PANNING_TAG, - QUAD_TAG, - QUANTIZER_TAG, - RANDOM_TAG, - REVERB_TAG, - RING_MODULATOR_TAG, - SAMPLE_AND_HOLD_TAG, - SAMPLER_TAG, - SEQUENCER_TAG, - SLEW_LIMITER_TAG, - SWITCH_TAG, - SYNTH_VOICE_TAG, - TUNER_TAG, - UTILITY_TAG, - VISUAL_TAG, - WAVESHAPER_TAG, - NUM_TAGS -}; - - struct ModuleWidget; struct Model; @@ -72,11 +25,12 @@ struct Plugin { */ std::string slug; - /** The version of your plugin (optional) + /** The version of your plugin Plugins should follow the versioning scheme described at https://github.com/VCVRack/Rack/issues/266 Do not include the "v" in "v1.0" for example. */ std::string version; + /** URL for plugin homepage (optional) */ std::string website; /** URL for plugin manual (optional) */ @@ -110,7 +64,8 @@ void pluginInit(); void pluginDestroy(); void pluginLogIn(std::string email, std::string password); void pluginLogOut(); -void pluginRefresh(); +/** Returns whether a new plugin is available, and downloads it unless doing a dry run */ +bool pluginSync(bool dryRun); void pluginCancelDownload(); bool pluginIsLoggedIn(); bool pluginIsDownloading(); @@ -121,7 +76,6 @@ std::string pluginGetLoginStatus(); extern std::list gPlugins; extern std::string gToken; -extern std::string gTagNames[NUM_TAGS]; } // namespace rack diff --git a/include/tags.hpp b/include/tags.hpp new file mode 100644 index 00000000..cbf9a2d0 --- /dev/null +++ b/include/tags.hpp @@ -0,0 +1,72 @@ +#pragma once +#include + + +namespace rack { + + +/** Describes the type(s) of each module +To see comments, turn word wrap on. I'm using inline comments so I can automatically sort the list when more tags are added. +*/ +enum ModelTag { + AMPLIFIER_TAG, + ATTENUATOR_TAG, + BLANK_TAG, + CHORUS_TAG, + CLOCK_TAG, + COMPRESSOR_TAG, + CONTROLLER_TAG, // Use only if the artist "performs" with this module. Knobs are not sufficient. Examples: on-screen keyboard, XY pad. + DELAY_TAG, + DIGITAL_TAG, + DISTORTION_TAG, + DRUM_TAG, + DUAL_TAG, // The core functionality times two. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Dual module. + DYNAMICS_TAG, + EFFECT_TAG, + ENVELOPE_FOLLOWER_TAG, + ENVELOPE_GENERATOR_TAG, + EQUALIZER_TAG, + EXTERNAL_TAG, + FILTER_TAG, + FLANGER_TAG, + FUNCTION_GENERATOR_TAG, + GRANULAR_TAG, + LFO_TAG, + LIMITER_TAG, + LOGIC_TAG, + LOW_PASS_GATE_TAG, + MIDI_TAG, + MIXER_TAG, + MULTIPLE_TAG, + NOISE_TAG, + OSCILLATOR_TAG, + PANNING_TAG, + PHASER_TAG, + PHYSICAL_MODELING_TAG, + QUAD_TAG, // The core functionality times four. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Quad module. + QUANTIZER_TAG, + RANDOM_TAG, + RECORDING_TAG, + REVERB_TAG, + RING_MODULATOR_TAG, + SAMPLE_AND_HOLD_TAG, + SAMPLER_TAG, + SEQUENCER_TAG, + SLEW_LIMITER_TAG, + SWITCH_TAG, + SYNTH_VOICE_TAG, // A synth voice must have an envelope built-in. + TUNER_TAG, + UTILITY_TAG, // Serves only extremely basic functions, like inverting, max, min, multiplying by 2, etc. + VISUAL_TAG, + VOCODER_TAG, + WAVESHAPER_TAG, + NUM_TAGS +}; + + +void tagsInit(); + +extern std::string gTagNames[NUM_TAGS]; + + +} // namespace rack diff --git a/include/util.hpp b/include/util.hpp index f7e4732a..3e7b53dc 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -30,6 +30,16 @@ will expand to #define LENGTHOF(arr) (sizeof(arr) / sizeof((arr)[0])) +/** Reserve space for _count enums starting with _name. +Example: + enum Foo { + ENUMS(BAR, 14) + }; + + BAR + 0 to BAR + 11 is reserved +*/ +#define ENUMS(_name, _count) _name, _name ## _LAST = _name + (_count) - 1 + /** Deprecation notice for GCC */ #define DEPRECATED __attribute__ ((deprecated)) diff --git a/include/util/request.hpp b/include/util/request.hpp index d0405155..7ae510c7 100644 --- a/include/util/request.hpp +++ b/include/util/request.hpp @@ -19,5 +19,6 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ); /** Returns the filename, blank if unsuccessful */ bool requestDownload(std::string url, std::string filename, float *progress); std::string requestEscape(std::string s); +std::string requestSHA256File(std::string filename); } // namespace rack diff --git a/include/widgets.hpp b/include/widgets.hpp index b26245af..6c8fa9f3 100644 --- a/include/widgets.hpp +++ b/include/widgets.hpp @@ -178,6 +178,7 @@ struct ZoomWidget : Widget { void onMouseMove(EventMouseMove &e) override; void onHoverKey(EventHoverKey &e) override; void onScroll(EventScroll &e) override; + void onPathDrop(EventPathDrop &e) override; }; //////////////////// @@ -295,7 +296,7 @@ struct Label : Widget { /** Deletes itself from parent when clicked */ struct MenuOverlay : OpaqueWidget { void step() override; - void onDragDrop(EventDragDrop &e) override; + void onMouseDown(EventMouseDown &e) override; void onHoverKey(EventHoverKey &e) override; }; diff --git a/plugin.mk b/plugin.mk index 1977a1d0..b1d9e4cd 100644 --- a/plugin.mk +++ b/plugin.mk @@ -21,6 +21,8 @@ ifeq ($(ARCH), win) TARGET = plugin.dll endif +DISTRIBUTABLES += $(TARGET) + all: $(TARGET) @@ -28,3 +30,11 @@ include ../../compile.mk clean: rm -rfv build $(TARGET) dist + +dist: all + rm -rf dist + mkdir -p dist/$(SLUG) + cp -R $(DISTRIBUTABLES) dist/$(SLUG)/ + cd dist && zip -5 -r $(SLUG)-$(VERSION)-$(ARCH).zip $(SLUG) + +.PHONY: clean dist \ No newline at end of file diff --git a/res/ComponentLibrary/CKSSThree_0.svg b/res/ComponentLibrary/CKSSThree_0.svg new file mode 100644 index 00000000..a8283517 --- /dev/null +++ b/res/ComponentLibrary/CKSSThree_0.svg @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/res/ComponentLibrary/CKSSThree_1.svg b/res/ComponentLibrary/CKSSThree_1.svg new file mode 100644 index 00000000..46656fcf --- /dev/null +++ b/res/ComponentLibrary/CKSSThree_1.svg @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/res/ComponentLibrary/CKSSThree_2.svg b/res/ComponentLibrary/CKSSThree_2.svg new file mode 100644 index 00000000..cbfd986b --- /dev/null +++ b/res/ComponentLibrary/CKSSThree_2.svg @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/src/app.cpp b/src/app.cpp index 7d930a97..0a882c9f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -12,7 +12,8 @@ std::string gApplicationVersion = #else ""; #endif -std::string gApiHost = "http://api.vcvrack.com"; +std::string gApiHost = "https://api.vcvrack.com"; +// std::string gApiHost = "http://localhost:8081"; RackWidget *gRackWidget = NULL; Toolbar *gToolbar = NULL; diff --git a/src/app/AddModuleWindow.cpp b/src/app/AddModuleWindow.cpp index 5e6bf541..ac99389e 100644 --- a/src/app/AddModuleWindow.cpp +++ b/src/app/AddModuleWindow.cpp @@ -70,7 +70,7 @@ struct MetadataMenu : ListMenu { // Plugin metadata if (!model->plugin->website.empty()) { - addChild(construct(&MenuEntry::text, "Website", &UrlItem::url, model->plugin->path)); + addChild(construct(&MenuEntry::text, "Website", &UrlItem::url, model->plugin->website)); } if (!model->plugin->manual.empty()) { addChild(construct(&MenuEntry::text, "Manual", &UrlItem::url, model->plugin->manual)); @@ -120,10 +120,6 @@ struct ModelItem : MenuItem { sModel = model; MenuItem::onMouseEnter(e); } - void onMouseLeave(EventMouseLeave &e) override { - sModel = NULL; - MenuItem::onMouseLeave(e); - } }; @@ -183,6 +179,8 @@ struct ManufacturerMenu : ListMenu { std::string filter; ManufacturerMenu() { + addChild(construct(&MenuLabel::text, "Manufacturers")); + // Collect manufacturer names std::set manufacturers; for (Plugin *plugin : gPlugins) { @@ -205,7 +203,8 @@ struct ManufacturerMenu : ListMenu { // Make children with a matching model visible for (Widget *child : children) { MenuItem *item = dynamic_cast(child); - assert(item); + if (!item) + continue; std::string manufacturer = item->text; for (Plugin *plugin : gPlugins) { @@ -282,9 +281,6 @@ AddModuleWindow::AddModuleWindow() { metadataScroll->box.pos = Vec(400, posY); metadataScroll->box.size = Vec(200, box.size.y - posY); addChild(metadataScroll); - - // NVGcolor c = bndTransparent(nvgRGB(0, 0, 0)); - NVGcolor c = bndGetTheme()->nodeTheme.nodeBackdropColor; } diff --git a/src/app/Knob.cpp b/src/app/Knob.cpp index 4495b98e..f47f3349 100644 --- a/src/app/Knob.cpp +++ b/src/app/Knob.cpp @@ -18,7 +18,11 @@ void Knob::onDragStart(EventDragStart &e) { void Knob::onDragMove(EventDragMove &e) { // Drag slower if Mod - float delta = KNOB_SENSITIVITY * (maxValue - minValue) * -e.mouseRel.y; + float range = maxValue - minValue; + float delta = KNOB_SENSITIVITY * -e.mouseRel.y * speed; + if (std::isfinite(range)) + delta *= range; + if (guiIsModPressed()) delta /= 16.0; dragValue += delta; diff --git a/src/app/LightWidget.cpp b/src/app/LightWidget.cpp index a75bf23c..70f3a00d 100644 --- a/src/app/LightWidget.cpp +++ b/src/app/LightWidget.cpp @@ -5,35 +5,46 @@ namespace rack { void LightWidget::draw(NVGcontext *vg) { - float radius = box.size.x / 2.0; - float oradius = radius + 15.0; - color.r = clampf(color.r, 0.0, 1.0); color.g = clampf(color.g, 0.0, 1.0); color.b = clampf(color.b, 0.0, 1.0); color.a = clampf(color.a, 0.0, 1.0); - // Solid + drawLight(vg); + drawHalo(vg); +} + + +void LightWidget::drawLight(NVGcontext *vg) { + float radius = box.size.x / 2.0; + nvgBeginPath(vg); nvgCircle(vg, radius, radius, radius); + + // Background nvgFillColor(vg, bgColor); nvgFill(vg); - // Border - nvgStrokeWidth(vg, 1.0); - NVGcolor borderColor = bgColor; - borderColor.a *= 0.5; - nvgStrokeColor(vg, borderColor); - nvgStroke(vg); + // // Border + // nvgStrokeWidth(vg, 1.0); + // NVGcolor borderColor = bgColor; + // borderColor.a *= 0.5; + // nvgStrokeColor(vg, borderColor); + // nvgStroke(vg); // Inner glow - nvgGlobalCompositeOperation(vg, NVG_LIGHTER); nvgFillColor(vg, color); nvgFill(vg); +} + + +void LightWidget::drawHalo(NVGcontext *vg) { + float radius = box.size.x / 2.0; + float oradius = radius + 15.0; - // Outer glow nvgBeginPath(vg); nvgRect(vg, radius - oradius, radius - oradius, 2*oradius, 2*oradius); + NVGpaint paint; NVGcolor icol = color; icol.a *= 0.10; @@ -41,8 +52,11 @@ void LightWidget::draw(NVGcontext *vg) { ocol.a = 0.0; paint = nvgRadialGradient(vg, radius, radius, radius, oradius, icol, ocol); nvgFillPaint(vg, paint); + nvgGlobalCompositeOperation(vg, NVG_LIGHTER); nvgFill(vg); } + + } // namespace rack diff --git a/src/app/ModuleWidget.cpp b/src/app/ModuleWidget.cpp index 975cf835..bdd5e0d8 100644 --- a/src/app/ModuleWidget.cpp +++ b/src/app/ModuleWidget.cpp @@ -60,8 +60,11 @@ void ModuleWidget::setPanel(std::shared_ptr svg) { json_t *ModuleWidget::toJson() { json_t *rootJ = json_object(); - // manufacturer + // plugin json_object_set_new(rootJ, "plugin", json_string(model->plugin->slug.c_str())); + // version (of plugin) + if (!model->plugin->version.empty()) + json_object_set_new(rootJ, "version", json_string(model->plugin->version.c_str())); // model json_object_set_new(rootJ, "model", json_string(model->slug.c_str())); // pos @@ -118,12 +121,24 @@ void ModuleWidget::disconnect() { } } +void ModuleWidget::create() { + if (module) { + module->onCreate(); + } +} + +void ModuleWidget::_delete() { + if (module) { + module->onDelete(); + } +} + void ModuleWidget::reset() { for (ParamWidget *param : params) { param->setValue(param->defaultValue); } if (module) { - module->reset(); + module->onReset(); } } @@ -132,7 +147,7 @@ void ModuleWidget::randomize() { param->randomize(); } if (module) { - module->randomize(); + module->onRandomize(); } } @@ -188,7 +203,7 @@ void ModuleWidget::onMouseMove(EventMouseMove &e) { gRackWidget->deleteModule(this); this->finalizeEvents(); delete this; - // Kinda sketchy because events will be passed further down the tree + e.consumed = true; return; } } @@ -279,36 +294,36 @@ Menu *ModuleWidget::createContextMenu() { MenuLabel *menuLabel = new MenuLabel(); menuLabel->text = model->manufacturer + " " + model->name; - menu->pushChild(menuLabel); + menu->addChild(menuLabel); ResetMenuItem *resetItem = new ResetMenuItem(); resetItem->text = "Initialize"; resetItem->rightText = GUI_MOD_KEY_NAME "+I"; resetItem->moduleWidget = this; - menu->pushChild(resetItem); + menu->addChild(resetItem); RandomizeMenuItem *randomizeItem = new RandomizeMenuItem(); randomizeItem->text = "Randomize"; randomizeItem->rightText = GUI_MOD_KEY_NAME "+R"; randomizeItem->moduleWidget = this; - menu->pushChild(randomizeItem); + menu->addChild(randomizeItem); DisconnectMenuItem *disconnectItem = new DisconnectMenuItem(); disconnectItem->text = "Disconnect cables"; disconnectItem->moduleWidget = this; - menu->pushChild(disconnectItem); + menu->addChild(disconnectItem); CloneMenuItem *cloneItem = new CloneMenuItem(); cloneItem->text = "Duplicate"; cloneItem->rightText = GUI_MOD_KEY_NAME "+D"; cloneItem->moduleWidget = this; - menu->pushChild(cloneItem); + menu->addChild(cloneItem); DeleteMenuItem *deleteItem = new DeleteMenuItem(); deleteItem->text = "Delete"; deleteItem->rightText = "Backspace/Delete"; deleteItem->moduleWidget = this; - menu->pushChild(deleteItem); + menu->addChild(deleteItem); return menu; } diff --git a/src/app/PluginManagerWidget.cpp b/src/app/PluginManagerWidget.cpp index c246c325..096eb071 100644 --- a/src/app/PluginManagerWidget.cpp +++ b/src/app/PluginManagerWidget.cpp @@ -1,11 +1,57 @@ #include #include "app.hpp" #include "plugin.hpp" +#include "gui.hpp" +#include "../ext/osdialog/osdialog.h" namespace rack { +struct SyncButton : Button { + bool checked = false; + bool available = false; + bool completed = false; + + void step() override { + if (!checked) { + std::thread t([this]() { + if (pluginSync(true)) + available = true; + }); + t.detach(); + checked = true; + } + if (completed) { + if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { + guiClose(); + } + completed = false; + } + } + void draw(NVGcontext *vg) override { + Button::draw(vg); + if (available) { + // Notification circle + nvgBeginPath(vg); + nvgCircle(vg, 3, 3, 4.0); + nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0)); + nvgFill(vg); + nvgStrokeColor(vg, nvgRGBf(0.5, 0.0, 0.0)); + nvgStroke(vg); + } + } + void onAction(EventAction &e) override { + available = false; + std::thread t([this]() { + if (pluginSync(false)) + completed = true; + }); + t.detach(); + } +}; + + PluginManagerWidget::PluginManagerWidget() { box.size.y = BND_WIDGET_HEIGHT; float margin = 5; @@ -91,19 +137,13 @@ PluginManagerWidget::PluginManagerWidget() { manageWidget->addChild(manageButton); pos.x += manageButton->box.size.x; - struct RefreshButton : Button { - void onAction(EventAction &e) override { - std::thread t(pluginRefresh); - t.detach(); - } - }; pos.x += margin; - Button *refreshButton = new RefreshButton(); - refreshButton->box.pos = pos; - refreshButton->box.size.x = 125; - refreshButton->text = "Refresh plugins"; - manageWidget->addChild(refreshButton); - pos.x += refreshButton->box.size.x; + Button *syncButton = new SyncButton(); + syncButton->box.pos = pos; + syncButton->box.size.x = 125; + syncButton->text = "Update plugins"; + manageWidget->addChild(syncButton); + pos.x += syncButton->box.size.x; struct LogOutButton : Button { void onAction(EventAction &e) override { diff --git a/src/app/RackRail.cpp b/src/app/RackRail.cpp index e126f38a..8dc97e27 100644 --- a/src/app/RackRail.cpp +++ b/src/app/RackRail.cpp @@ -46,6 +46,15 @@ void RackRail::draw(NVGcontext *vg) { nvgLineTo(vg, box.size.x, railY + RACK_GRID_HEIGHT - 0.5); nvgStroke(vg); } + + + // Useful for screenshots + if (0) { + nvgBeginPath(vg); + nvgRect(vg, 0.0, 0.0, box.size.x, box.size.y); + nvgFillColor(vg, nvgRGBf(1.0, 1.0, 1.0)); + nvgFill(vg); + } } diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index 7c6cf7c8..f9e9db26 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -122,7 +122,6 @@ void RackScene::onHoverKey(EventHoverKey &e) { Widget::onHoverKey(e); } - void RackScene::onPathDrop(EventPathDrop &e) { if (e.paths.size() >= 1) { const std::string& firstPath = e.paths.front(); @@ -131,8 +130,10 @@ void RackScene::onPathDrop(EventPathDrop &e) { e.consumed = true; } } -} + if (!e.consumed) + Scene::onPathDrop(e); +} } // namespace rack diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index f8301de5..d01cf012 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -38,11 +38,32 @@ void RackWidget::clear() { wireContainer->clearChildren(); moduleContainer->clearChildren(); lastPath = ""; + +/* + // Add all modules to rack + Vec pos; + for (Plugin *plugin : gPlugins) { + for (Model *model : plugin->models) { + ModuleWidget *moduleWidget = model->createModuleWidget(); + moduleContainer->addChild(moduleWidget); + // Move module nearest to the mouse position + Rect box; + box.size = moduleWidget->box.size; + box.pos = pos; + requestModuleBoxNearest(moduleWidget, box); + pos.x += box.size.x; + } + pos.y += RACK_GRID_HEIGHT; + pos.x = 0; + } +*/ } void RackWidget::reset() { - clear(); - loadPatch(assetLocal("template.vcv")); + if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "Clear your patch and start over?")) { + clear(); + loadPatch(assetLocal("template.vcv")); + } } void RackWidget::openDialog() { @@ -81,20 +102,19 @@ void RackWidget::saveAsDialog() { } } - void RackWidget::savePatch(std::string path) { info("Saving patch %s", path.c_str()); - FILE *file = fopen(path.c_str(), "w"); - if (!file) + json_t *rootJ = toJson(); + if (!rootJ) return; - json_t *rootJ = toJson(); - if (rootJ) { + FILE *file = fopen(path.c_str(), "w"); + if (file) { json_dumpf(rootJ, file, JSON_INDENT(2)); - json_decref(rootJ); + fclose(file); } - fclose(file); + json_decref(rootJ); } void RackWidget::loadPatch(std::string path) { @@ -125,8 +145,10 @@ json_t *RackWidget::toJson() { json_t *rootJ = json_object(); // version - json_t *versionJ = json_string(gApplicationVersion.c_str()); - json_object_set_new(rootJ, "version", versionJ); + if (!gApplicationVersion.empty()) { + json_t *versionJ = json_string(gApplicationVersion.c_str()); + json_object_set_new(rootJ, "version", versionJ); + } // modules json_t *modulesJ = json_array(); @@ -283,9 +305,11 @@ void RackWidget::fromJson(json_t *rootJ) { void RackWidget::addModule(ModuleWidget *m) { moduleContainer->addChild(m); + m->create(); } void RackWidget::deleteModule(ModuleWidget *m) { + m->_delete(); moduleContainer->removeChild(m); } @@ -321,8 +345,8 @@ bool RackWidget::requestModuleBoxNearest(ModuleWidget *m, Rect box) { int x0 = roundf(box.pos.x / RACK_GRID_WIDTH); int y0 = roundf(box.pos.y / RACK_GRID_HEIGHT); std::vector positions; - for (int y = maxi(0, y0 - 4); y < y0 + 4; y++) { - for (int x = maxi(0, x0 - 200); x < x0 + 200; x++) { + for (int y = maxi(0, y0 - 8); y < y0 + 8; y++) { + for (int x = maxi(0, x0 - 400); x < x0 + 400; x++) { positions.push_back(Vec(x * RACK_GRID_WIDTH, y * RACK_GRID_HEIGHT)); } } diff --git a/src/app/SVGSlider.cpp b/src/app/SVGFader.cpp similarity index 78% rename from src/app/SVGSlider.cpp rename to src/app/SVGFader.cpp index 5d0beb60..ee170ea1 100644 --- a/src/app/SVGSlider.cpp +++ b/src/app/SVGFader.cpp @@ -4,7 +4,7 @@ namespace rack { -SVGSlider::SVGSlider() { +SVGFader::SVGFader() { background = new SVGWidget(); addChild(background); @@ -12,7 +12,7 @@ SVGSlider::SVGSlider() { addChild(handle); } -void SVGSlider::step() { +void SVGFader::step() { if (dirty) { // Update handle position Vec handlePos = Vec(rescalef(value, minValue, maxValue, minHandlePos.x, maxHandlePos.x), rescalef(value, minValue, maxValue, minHandlePos.y, maxHandlePos.y)); @@ -21,9 +21,9 @@ void SVGSlider::step() { FramebufferWidget::step(); } -void SVGSlider::onChange(EventChange &e) { +void SVGFader::onChange(EventChange &e) { dirty = true; - ParamWidget::onChange(e); + Knob::onChange(e); } diff --git a/src/app/SVGKnob.cpp b/src/app/SVGKnob.cpp index ae25ddb3..fc96b131 100644 --- a/src/app/SVGKnob.cpp +++ b/src/app/SVGKnob.cpp @@ -23,7 +23,9 @@ void SVGKnob::step() { // Re-transform TransformWidget if dirty if (dirty) { tw->box.size = box.size; - float angle = rescalef(value, minValue, maxValue, minAngle, maxAngle); + float angle = 0.0; + if (std::isfinite(minValue) && std::isfinite(maxValue)) + angle = rescalef(value, minValue, maxValue, minAngle, maxAngle); tw->identity(); // Scale SVG to box tw->scale(box.size.div(sw->box.size)); diff --git a/src/app/SVGSwitch.cpp b/src/app/SVGSwitch.cpp index 5b66518d..eecaeb1e 100644 --- a/src/app/SVGSwitch.cpp +++ b/src/app/SVGSwitch.cpp @@ -18,13 +18,10 @@ void SVGSwitch::addFrame(std::shared_ptr svg) { } } -void SVGSwitch::step() { - FramebufferWidget::step(); -} - void SVGSwitch::onChange(EventChange &e) { assert(frames.size() > 0); - int index = clampi((int) roundf(value), 0, frames.size() - 1); + float valueScaled = rescalef(value, minValue, maxValue, 0, frames.size() - 1); + int index = clampi((int) roundf(valueScaled), 0, frames.size() - 1); sw->setSVG(frames[index]); dirty = true; Switch::onChange(e); diff --git a/src/app/Toolbar.cpp b/src/app/Toolbar.cpp index cde1221b..6fe03649 100644 --- a/src/app/Toolbar.cpp +++ b/src/app/Toolbar.cpp @@ -43,11 +43,11 @@ struct FileChoice : ChoiceButton { menu->box.size.x = box.size.x; { - menu->pushChild(construct(&MenuItem::text, "New", &MenuItem::rightText, GUI_MOD_KEY_NAME "+N")); - menu->pushChild(construct(&MenuItem::text, "Open", &MenuItem::rightText, GUI_MOD_KEY_NAME "+O")); - menu->pushChild(construct(&MenuItem::text, "Save", &MenuItem::rightText, GUI_MOD_KEY_NAME "+S")); - menu->pushChild(construct(&MenuItem::text, "Save as", &MenuItem::rightText, GUI_MOD_KEY_NAME "+Shift+S")); - menu->pushChild(construct(&MenuItem::text, "Quit", &MenuItem::rightText, GUI_MOD_KEY_NAME "+Q")); + menu->addChild(construct(&MenuItem::text, "New", &MenuItem::rightText, GUI_MOD_KEY_NAME "+N")); + menu->addChild(construct(&MenuItem::text, "Open", &MenuItem::rightText, GUI_MOD_KEY_NAME "+O")); + menu->addChild(construct(&MenuItem::text, "Save", &MenuItem::rightText, GUI_MOD_KEY_NAME "+S")); + menu->addChild(construct(&MenuItem::text, "Save as", &MenuItem::rightText, GUI_MOD_KEY_NAME "+Shift+S")); + menu->addChild(construct(&MenuItem::text, "Quit", &MenuItem::rightText, GUI_MOD_KEY_NAME "+Q")); } } }; @@ -75,7 +75,7 @@ struct SampleRateChoice : ChoiceButton { PauseItem *pauseItem = new PauseItem(); pauseItem->text = gPaused ? "Resume engine" : "Pause engine"; - menu->pushChild(pauseItem); + menu->addChild(pauseItem); float sampleRates[] = {44100, 48000, 88200, 96000, 176400, 192000}; int sampleRatesLen = sizeof(sampleRates) / sizeof(sampleRates[0]); @@ -83,7 +83,7 @@ struct SampleRateChoice : ChoiceButton { SampleRateItem *item = new SampleRateItem(); item->text = stringf("%.0f Hz", sampleRates[i]); item->sampleRate = sampleRates[i]; - menu->pushChild(item); + menu->addChild(item); } } void step() override { @@ -151,12 +151,13 @@ Toolbar::Toolbar() { struct ZoomSlider : Slider { void onAction(EventAction &e) override { Slider::onAction(e); - gRackScene->zoomWidget->setZoom(value / 100.0); + gRackScene->zoomWidget->setZoom(roundf(value) / 100.0); } }; zoomSlider = new ZoomSlider(); zoomSlider->box.pos = Vec(xPos, margin); zoomSlider->box.size.x = 150; + zoomSlider->precision = 0; zoomSlider->label = "Zoom"; zoomSlider->unit = "%"; zoomSlider->setLimits(25.0, 200.0); diff --git a/src/app/WireWidget.cpp b/src/app/WireWidget.cpp index 07d16226..75b73516 100644 --- a/src/app/WireWidget.cpp +++ b/src/app/WireWidget.cpp @@ -86,11 +86,6 @@ static int lastWireColorId = -1; WireWidget::WireWidget() { lastWireColorId = (lastWireColorId + 1) % LENGTHOF(wireColors); color = wireColors[lastWireColorId]; - - // inputLight = construct(&PolarityLight::posColor, COLOR_GREEN, &PolarityLight::negColor, COLOR_RED); - // outputLight = construct(&PolarityLight::posColor, COLOR_GREEN, &PolarityLight::negColor, COLOR_RED); - // addChild(inputLight); - // addChild(outputLight); } WireWidget::~WireWidget() { diff --git a/src/core/AudioInterface.cpp b/src/core/AudioInterface.cpp index 6fcfd373..b9bdfdca 100644 --- a/src/core/AudioInterface.cpp +++ b/src/core/AudioInterface.cpp @@ -1,13 +1,14 @@ #include #include #include +#include #include "core.hpp" #include "dsp/samplerate.hpp" #include "dsp/ringbuffer.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsuggest-override" -#include +#include #pragma GCC diagnostic pop @@ -28,18 +29,14 @@ struct AudioInterface : Module { NUM_OUTPUTS = AUDIO1_OUTPUT + 8 }; - RtAudio stream; + RtAudio *stream = NULL; // Stream properties - int deviceId = -1; + int device = -1; float sampleRate = 44100.0; int blockSize = 256; int numOutputs = 0; int numInputs = 0; - // Used because the GUI thread and Rack thread can both interact with this class - std::mutex bufferMutex; - bool streamRunning; - SampleRateConverter<8> inputSrc; SampleRateConverter<8> outputSrc; @@ -49,85 +46,116 @@ struct AudioInterface : Module { // in device's sample rate DoubleRingBuffer, (1<<15)> inputSrcBuffer; - AudioInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {} + AudioInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + setDriver(RtAudio::UNSPECIFIED); + } ~AudioInterface() { - closeDevice(); + closeStream(); } void step() override; void stepStream(const float *input, float *output, int numFrames); int getDeviceCount(); - std::string getDeviceName(int deviceId); - - void openDevice(int deviceId, float sampleRate, int blockSize); - void closeDevice(); - - void setDeviceId(int deviceId) { - openDevice(deviceId, sampleRate, blockSize); - } - void setSampleRate(float sampleRate) { - openDevice(deviceId, sampleRate, blockSize); - } - void setBlockSize(int blockSize) { - openDevice(deviceId, sampleRate, blockSize); + std::string getDeviceName(int device); + + void openStream(); + void closeStream(); + + void setDriver(int driver) { + closeStream(); + if (stream) + delete stream; + stream = new RtAudio((RtAudio::Api) driver); + } + int getDriver() { + if (!stream) + return RtAudio::UNSPECIFIED; + return stream->getCurrentApi(); + } + std::vector getAvailableDrivers() { + std::vector apis; + RtAudio::getCompiledApi(apis); + std::vector drivers; + for (RtAudio::Api api : apis) + drivers.push_back(api); + return drivers; + } + std::string getDriverName(int driver) { + switch (driver) { + case RtAudio::UNSPECIFIED: return "Unspecified"; + case RtAudio::LINUX_ALSA: return "ALSA"; + case RtAudio::LINUX_PULSE: return "PulseAudio"; + case RtAudio::LINUX_OSS: return "OSS"; + case RtAudio::UNIX_JACK: return "JACK"; + case RtAudio::MACOSX_CORE: return "Core Audio"; + case RtAudio::WINDOWS_WASAPI: return "WASAPI"; + case RtAudio::WINDOWS_ASIO: return "ASIO"; + case RtAudio::WINDOWS_DS: return "DirectSound"; + case RtAudio::RTAUDIO_DUMMY: return "Dummy"; + default: return "Unknown"; + } } + std::vector getSampleRates(); + json_t *toJson() override { json_t *rootJ = json_object(); - if (deviceId >= 0) { - std::string deviceName = getDeviceName(deviceId); - json_object_set_new(rootJ, "deviceName", json_string(deviceName.c_str())); - json_object_set_new(rootJ, "sampleRate", json_real(sampleRate)); - json_object_set_new(rootJ, "blockSize", json_integer(blockSize)); - } + json_object_set_new(rootJ, "driver", json_integer(getDriver())); + json_object_set_new(rootJ, "device", json_integer(device)); + json_object_set_new(rootJ, "sampleRate", json_real(sampleRate)); + json_object_set_new(rootJ, "blockSize", json_integer(blockSize)); return rootJ; } void fromJson(json_t *rootJ) override { - json_t *deviceNameJ = json_object_get(rootJ, "deviceName"); - if (deviceNameJ) { - std::string deviceName = json_string_value(deviceNameJ); - for (int i = 0; i < getDeviceCount(); i++) { - if (deviceName == getDeviceName(i)) { - setDeviceId(i); - break; - } - } - } + json_t *driverJ = json_object_get(rootJ, "driver"); + if (driverJ) + setDriver(json_number_value(driverJ)); + + json_t *deviceJ = json_object_get(rootJ, "device"); + if (deviceJ) + device = json_number_value(deviceJ); json_t *sampleRateJ = json_object_get(rootJ, "sampleRate"); - if (sampleRateJ) { - setSampleRate(json_number_value(sampleRateJ)); - } + if (sampleRateJ) + sampleRate = json_number_value(sampleRateJ); json_t *blockSizeJ = json_object_get(rootJ, "blockSize"); - if (blockSizeJ) { - setBlockSize(json_integer_value(blockSizeJ)); - } + if (blockSizeJ) + blockSize = json_integer_value(blockSizeJ); + + openStream(); } - void reset() override { - closeDevice(); + void onReset() override { + closeStream(); } }; +#define TIMED_SLEEP_LOCK(_cond, _spinTime, _totalTime) { \ + auto startTime = std::chrono::high_resolution_clock::now(); \ + while (!(_cond)) { \ + std::this_thread::sleep_for(std::chrono::duration(_spinTime)); \ + auto currTime = std::chrono::high_resolution_clock::now(); \ + float totalTime = std::chrono::duration(currTime - startTime).count(); \ + if (totalTime > (_totalTime)) \ + break; \ + } \ +} + + void AudioInterface::step() { + // debug("inputBuffer %d inputSrcBuffer %d outputBuffer %d", inputBuffer.size(), inputSrcBuffer.size(), outputBuffer.size()); // Read/write stream if we have enough input, OR the output buffer is empty if we have no input if (numOutputs > 0) { - while (inputSrcBuffer.size() >= blockSize && streamRunning) { - std::this_thread::sleep_for(std::chrono::duration(100e-6)); - } + TIMED_SLEEP_LOCK(inputSrcBuffer.size() < blockSize, 100e-6, 0.2); } else if (numInputs > 0) { - while (outputBuffer.empty() && streamRunning) { - std::this_thread::sleep_for(std::chrono::duration(100e-6)); - } + TIMED_SLEEP_LOCK(!outputBuffer.empty(), 100e-6, 0.2); } - std::lock_guard lock(bufferMutex); - // Get input and pass it through the sample rate converter if (numOutputs > 0) { if (!inputBuffer.full()) { @@ -141,7 +169,7 @@ void AudioInterface::step() { // Once full, sample rate convert the input // inputBuffer -> SRC -> inputSrcBuffer if (inputBuffer.full()) { - inputSrc.setRatio(sampleRate / engineGetSampleRate()); + inputSrc.setRates(engineGetSampleRate(), sampleRate); int inLen = inputBuffer.size(); int outLen = inputSrcBuffer.capacity(); inputSrc.process(inputBuffer.startData(), &inLen, inputSrcBuffer.endData(), &outLen); @@ -160,16 +188,19 @@ void AudioInterface::step() { } void AudioInterface::stepStream(const float *input, float *output, int numFrames) { + if (gPaused) { + memset(output, 0, sizeof(float) * numOutputs * numFrames); + return; + } + if (numOutputs > 0) { // Wait for enough input before proceeding - while (inputSrcBuffer.size() < numFrames) { - if (!streamRunning) - return; - std::this_thread::sleep_for(std::chrono::duration(100e-6)); - } + TIMED_SLEEP_LOCK(inputSrcBuffer.size() >= numFrames, 100e-6, 0.2); + } + else if (numInputs > 0) { + TIMED_SLEEP_LOCK(outputBuffer.empty(), 100e-6, 0.2); } - std::lock_guard lock(bufferMutex); // input stream -> output buffer if (numInputs > 0) { Frame<8> inputFrames[numFrames]; @@ -180,7 +211,7 @@ void AudioInterface::stepStream(const float *input, float *output, int numFrames } // Pass output through sample rate converter - outputSrc.setRatio(engineGetSampleRate() / sampleRate); + outputSrc.setRates(sampleRate, engineGetSampleRate()); int inLen = numFrames; int outLen = outputBuffer.capacity(); outputSrc.process(inputFrames, &inLen, outputBuffer.endData(), &outLen); @@ -190,27 +221,32 @@ void AudioInterface::stepStream(const float *input, float *output, int numFrames // input buffer -> output stream if (numOutputs > 0) { for (int i = 0; i < numFrames; i++) { - if (inputSrcBuffer.empty()) - break; - Frame<8> f = inputSrcBuffer.shift(); + Frame<8> f; + if (inputSrcBuffer.empty()) { + memset(&f, 0, sizeof(f)); + } + else { + f = inputSrcBuffer.shift(); + } for (int c = 0; c < numOutputs; c++) { - output[i*numOutputs + c] = (c < 8) ? clampf(f.samples[c], -1.0, 1.0) : 0.0; + output[i*numOutputs + c] = clampf(f.samples[c], -1.0, 1.0); } } } } int AudioInterface::getDeviceCount() { - return stream.getDeviceCount(); + if (!stream) + return 0; + return stream->getDeviceCount(); } -std::string AudioInterface::getDeviceName(int deviceId) { - if (deviceId < 0) +std::string AudioInterface::getDeviceName(int device) { + if (!stream || device < 0) return ""; - std::lock_guard lock(bufferMutex); try { - RtAudio::DeviceInfo deviceInfo = stream.getDeviceInfo(deviceId); + RtAudio::DeviceInfo deviceInfo = stream->getDeviceInfo(device); return stringf("%s (%d in, %d out)", deviceInfo.name.c_str(), deviceInfo.inputChannels, deviceInfo.outputChannels); } catch (RtAudioError &e) { @@ -226,18 +262,17 @@ static int rtCallback(void *outputBuffer, void *inputBuffer, unsigned int nFrame return 0; } -void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { - closeDevice(); - std::lock_guard lock(bufferMutex); - - this->sampleRate = sampleRate; - this->blockSize = blockSize; +void AudioInterface::openStream() { + int device = this->device; + closeStream(); + if (!stream) + return; // Open new device - if (deviceId >= 0) { + if (device >= 0) { RtAudio::DeviceInfo deviceInfo; try { - deviceInfo = stream.getDeviceInfo(deviceId); + deviceInfo = stream->getDeviceInfo(device); } catch (RtAudioError &e) { warn("Failed to query audio device: %s", e.what()); @@ -247,23 +282,37 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { numOutputs = mini(deviceInfo.outputChannels, 8); numInputs = mini(deviceInfo.inputChannels, 8); + if (numOutputs == 0 && numInputs == 0) { + warn("Audio device %d has 0 inputs and 0 outputs"); + return; + } + RtAudio::StreamParameters outParameters; - outParameters.deviceId = deviceId; + outParameters.deviceId = device; outParameters.nChannels = numOutputs; RtAudio::StreamParameters inParameters; - inParameters.deviceId = deviceId; + inParameters.deviceId = device; inParameters.nChannels = numInputs; RtAudio::StreamOptions options; - options.flags |= RTAUDIO_MINIMIZE_LATENCY; + // options.flags |= RTAUDIO_SCHEDULE_REALTIME; + + // Find closest sample rate + unsigned int closestSampleRate = 0; + for (unsigned int sr : deviceInfo.sampleRates) { + if (fabsf(sr - sampleRate) < fabsf(closestSampleRate - sampleRate)) { + closestSampleRate = sr; + } + } try { // Don't use stream parameters if 0 input or output channels - stream.openStream( + debug("Opening audio stream %d", device); + stream->openStream( numOutputs == 0 ? NULL : &outParameters, numInputs == 0 ? NULL : &inParameters, - RTAUDIO_FLOAT32, sampleRate, (unsigned int*) &blockSize, &rtCallback, this, &options, NULL); + RTAUDIO_FLOAT32, closestSampleRate, (unsigned int*) &blockSize, &rtCallback, this, &options, NULL); } catch (RtAudioError &e) { warn("Failed to open audio stream: %s", e.what()); @@ -271,37 +320,44 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { } try { - stream.startStream(); + debug("Starting audio stream %d", device); + stream->startStream(); } catch (RtAudioError &e) { warn("Failed to start audio stream: %s", e.what()); return; } - streamRunning = true; - - this->sampleRate = stream.getStreamSampleRate(); - this->deviceId = deviceId; + // Update sample rate because this may have changed + this->sampleRate = stream->getStreamSampleRate(); + this->device = device; } } -void AudioInterface::closeDevice() { - std::lock_guard lock(bufferMutex); - - if (stream.isStreamOpen()) { - streamRunning = false; - try { - stream.abortStream(); - stream.closeStream(); +void AudioInterface::closeStream() { + if (stream) { + if (stream->isStreamRunning()) { + debug("Aborting audio stream %d", device); + try { + stream->abortStream(); + } + catch (RtAudioError &e) { + warn("Failed to abort stream %s", e.what()); + } } - catch (RtAudioError &e) { - warn("Failed to abort stream %s", e.what()); - return; + if (stream->isStreamOpen()) { + debug("Closing audio stream %d", device); + try { + stream->closeStream(); + } + catch (RtAudioError &e) { + warn("Failed to close stream %s", e.what()); + } } } // Reset stream settings - deviceId = -1; + device = -1; numOutputs = 0; numInputs = 0; @@ -313,16 +369,71 @@ void AudioInterface::closeDevice() { outputSrc.reset(); } +std::vector AudioInterface::getSampleRates() { + std::vector allowedSampleRates = {44100, 48000, 88200, 96000, 176400, 192000}; + if (!stream || device < 0) + return allowedSampleRates; -struct AudioItem : MenuItem { + try { + std::vector sampleRates; + RtAudio::DeviceInfo deviceInfo = stream->getDeviceInfo(device); + for (int sr : deviceInfo.sampleRates) { + float sampleRate = sr; + auto allowedIt = std::find(allowedSampleRates.begin(), allowedSampleRates.end(), sampleRate); + if (allowedIt != allowedSampleRates.end()) { + sampleRates.push_back(sampleRate); + } + } + return sampleRates; + } + catch (RtAudioError &e) { + warn("Failed to query audio device: %s", e.what()); + return {}; + } +} + + + +struct AudioDriverItem : MenuItem { AudioInterface *audioInterface; - int deviceId; + int driver; void onAction(EventAction &e) override { - audioInterface->setDeviceId(deviceId); + audioInterface->setDriver(driver); } }; -struct AudioChoice : ChoiceButton { +struct AudioDriverChoice : ChoiceButton { + AudioInterface *audioInterface; + void onAction(EventAction &e) override { + Menu *menu = gScene->createMenu(); + menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)).round(); + menu->box.size.x = box.size.x; + + for (int driver : audioInterface->getAvailableDrivers()) { + AudioDriverItem *audioItem = new AudioDriverItem(); + audioItem->audioInterface = audioInterface; + audioItem->driver = driver; + audioItem->text = audioInterface->getDriverName(driver); + menu->addChild(audioItem); + } + } + void step() override { + text = audioInterface->getDriverName(audioInterface->getDriver()); + } +}; + + +struct AudioDeviceItem : MenuItem { + AudioInterface *audioInterface; + int device; + void onAction(EventAction &e) override { + audioInterface->device = device; + audioInterface->openStream(); + } +}; + +struct AudioDeviceChoice : ChoiceButton { + int lastDeviceId = -1; AudioInterface *audioInterface; void onAction(EventAction &e) override { Menu *menu = gScene->createMenu(); @@ -331,23 +442,26 @@ struct AudioChoice : ChoiceButton { int deviceCount = audioInterface->getDeviceCount(); { - AudioItem *audioItem = new AudioItem(); + AudioDeviceItem *audioItem = new AudioDeviceItem(); audioItem->audioInterface = audioInterface; - audioItem->deviceId = -1; + audioItem->device = -1; audioItem->text = "No device"; - menu->pushChild(audioItem); + menu->addChild(audioItem); } - for (int deviceId = 0; deviceId < deviceCount; deviceId++) { - AudioItem *audioItem = new AudioItem(); + for (int device = 0; device < deviceCount; device++) { + AudioDeviceItem *audioItem = new AudioDeviceItem(); audioItem->audioInterface = audioInterface; - audioItem->deviceId = deviceId; - audioItem->text = audioInterface->getDeviceName(deviceId); - menu->pushChild(audioItem); + audioItem->device = device; + audioItem->text = audioInterface->getDeviceName(device); + menu->addChild(audioItem); } } void step() override { - std::string name = audioInterface->getDeviceName(audioInterface->deviceId); - text = ellipsize(name, 24); + if (lastDeviceId != audioInterface->device) { + std::string name = audioInterface->getDeviceName(audioInterface->device); + text = ellipsize(name, 24); + lastDeviceId = audioInterface->device; + } } }; @@ -356,7 +470,8 @@ struct SampleRateItem : MenuItem { AudioInterface *audioInterface; float sampleRate; void onAction(EventAction &e) override { - audioInterface->setSampleRate(sampleRate); + audioInterface->sampleRate = sampleRate; + audioInterface->openStream(); } }; @@ -367,14 +482,12 @@ struct SampleRateChoice : ChoiceButton { menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)).round(); menu->box.size.x = box.size.x; - const float sampleRates[6] = {44100, 48000, 88200, 96000, 176400, 192000}; - int sampleRatesLen = sizeof(sampleRates) / sizeof(sampleRates[0]); - for (int i = 0; i < sampleRatesLen; i++) { + for (float sampleRate : audioInterface->getSampleRates()) { SampleRateItem *item = new SampleRateItem(); item->audioInterface = audioInterface; - item->sampleRate = sampleRates[i]; - item->text = stringf("%.0f Hz", sampleRates[i]); - menu->pushChild(item); + item->sampleRate = sampleRate; + item->text = stringf("%.0f Hz", sampleRate); + menu->addChild(item); } } void step() override { @@ -387,7 +500,8 @@ struct BlockSizeItem : MenuItem { AudioInterface *audioInterface; int blockSize; void onAction(EventAction &e) override { - audioInterface->setBlockSize(blockSize); + audioInterface->blockSize = blockSize; + audioInterface->openStream(); } }; @@ -405,7 +519,7 @@ struct BlockSizeChoice : ChoiceButton { item->audioInterface = audioInterface; item->blockSize = blockSizes[i]; item->text = stringf("%d", blockSizes[i]); - menu->pushChild(item); + menu->addChild(item); } } void step() override { @@ -429,62 +543,77 @@ AudioInterfaceWidget::AudioInterfaceWidget() { // addChild(createScrew(Vec(15, 365))); // addChild(createScrew(Vec(box.size.x-30, 365))); - float margin = 5; + Vec margin = Vec(5, 2); float labelHeight = 15; - float yPos = margin; + float yPos = margin.y; float xPos; { Label *label = new Label(); - label->box.pos = Vec(margin, yPos); + label->box.pos = Vec(margin.x, yPos); + label->text = "Audio driver"; + addChild(label); + yPos += labelHeight + margin.y; + + AudioDriverChoice *choice = new AudioDriverChoice(); + choice->audioInterface = module; + choice->box.pos = Vec(margin.x, yPos); + choice->box.size.x = box.size.x - 2*margin.x; + addChild(choice); + yPos += choice->box.size.y + margin.y; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin.x, yPos); label->text = "Audio device"; addChild(label); - yPos += labelHeight + margin; + yPos += labelHeight + margin.y; - AudioChoice *choice = new AudioChoice(); - choice->audioInterface = dynamic_cast(module); - choice->box.pos = Vec(margin, yPos); - choice->box.size.x = box.size.x - 2*margin; + AudioDeviceChoice *choice = new AudioDeviceChoice(); + choice->audioInterface = module; + choice->box.pos = Vec(margin.x, yPos); + choice->box.size.x = box.size.x - 2*margin.x; addChild(choice); - yPos += choice->box.size.y + margin; + yPos += choice->box.size.y + margin.y; } { Label *label = new Label(); - label->box.pos = Vec(margin, yPos); + label->box.pos = Vec(margin.x, yPos); label->text = "Sample rate"; addChild(label); - yPos += labelHeight + margin; + yPos += labelHeight + margin.y; SampleRateChoice *choice = new SampleRateChoice(); - choice->audioInterface = dynamic_cast(module); - choice->box.pos = Vec(margin, yPos); - choice->box.size.x = box.size.x - 2*margin; + choice->audioInterface = module; + choice->box.pos = Vec(margin.x, yPos); + choice->box.size.x = box.size.x - 2*margin.x; addChild(choice); - yPos += choice->box.size.y + margin; + yPos += choice->box.size.y + margin.y; } { Label *label = new Label(); - label->box.pos = Vec(margin, yPos); + label->box.pos = Vec(margin.x, yPos); label->text = "Block size"; addChild(label); - yPos += labelHeight + margin; + yPos += labelHeight + margin.y; BlockSizeChoice *choice = new BlockSizeChoice(); - choice->audioInterface = dynamic_cast(module); - choice->box.pos = Vec(margin, yPos); - choice->box.size.x = box.size.x - 2*margin; + choice->audioInterface = module; + choice->box.pos = Vec(margin.x, yPos); + choice->box.size.x = box.size.x - 2*margin.x; addChild(choice); - yPos += choice->box.size.y + margin; + yPos += choice->box.size.y + margin.y; } { Label *label = new Label(); - label->box.pos = Vec(margin, yPos); + label->box.pos = Vec(margin.x, yPos); label->text = "Outputs"; addChild(label); - yPos += labelHeight + margin; + yPos += labelHeight + margin.y; } yPos += 5; @@ -496,9 +625,9 @@ AudioInterfaceWidget::AudioInterfaceWidget() { label->text = stringf("%d", i + 1); addChild(label); - xPos += 37 + margin; + xPos += 37 + margin.x; } - yPos += 35 + margin; + yPos += 35 + margin.y; yPos += 5; xPos = 10; @@ -509,16 +638,16 @@ AudioInterfaceWidget::AudioInterfaceWidget() { label->text = stringf("%d", i + 1); addChild(label); - xPos += 37 + margin; + xPos += 37 + margin.x; } - yPos += 35 + margin; + yPos += 35 + margin.y; { Label *label = new Label(); - label->box.pos = Vec(margin, yPos); + label->box.pos = Vec(margin.x, yPos); label->text = "Inputs"; addChild(label); - yPos += labelHeight + margin; + yPos += labelHeight + margin.y; } yPos += 5; @@ -530,9 +659,9 @@ AudioInterfaceWidget::AudioInterfaceWidget() { label->text = stringf("%d", i + 1); addChild(label); - xPos += 37 + margin; + xPos += 37 + margin.x; } - yPos += 35 + margin; + yPos += 35 + margin.y; yPos += 5; xPos = 10; @@ -543,7 +672,7 @@ AudioInterfaceWidget::AudioInterfaceWidget() { label->text = stringf("%d", i + 1); addChild(label); - xPos += 37 + margin; + xPos += 37 + margin.x; } - yPos += 35 + margin; + yPos += 35 + margin.y; } diff --git a/src/core/MidiCCToCV.cpp b/src/core/MidiCCToCV.cpp index 706b84b8..6e60e5f3 100644 --- a/src/core/MidiCCToCV.cpp +++ b/src/core/MidiCCToCV.cpp @@ -1,9 +1,9 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" + struct CCValue { int val = 0; // Controller value TransitionSmoother tSmooth; @@ -81,7 +81,7 @@ struct MIDICCToCVInterface : MidiIO, Module { } } - void reset() override { + void onReset() override { resetMidi(); } @@ -91,11 +91,11 @@ void MIDICCToCVInterface::step() { if (isPortOpen()) { std::vector message; + // midiIn->getMessage returns empty vector if there are no messages in the queue getMessage(&message); - while (message.size() > 0) { + if (message.size() > 0) { processMidi(message); - getMessage(&message); } } @@ -117,7 +117,7 @@ void MIDICCToCVInterface::resetMidi() { for (int i = 0; i < NUM_OUTPUTS; i++) { cc[i].val = 0; cc[i].resetSync(); - cc[i].tSmooth.set(0,0); + cc[i].tSmooth.set(0, 0); } }; @@ -147,7 +147,8 @@ void MIDICCToCVInterface::processMidi(std::vector msg) { cc[i].syncFirst = false; if (data2 < cc[i].val + 2 && data2 > cc[i].val - 2) { cc[i].sync = 0; - }else { + } + else { cc[i].sync = absi(data2 - cc[i].val); } return; @@ -156,7 +157,8 @@ void MIDICCToCVInterface::processMidi(std::vector msg) { if (cc[i].sync == 0) { cc[i].val = data2; cc[i].changed = true; - } else { + } + else { cc[i].sync = absi(data2 - cc[i].val); } } @@ -225,12 +227,14 @@ void CCTextField::onTextChange() { text = ""; begin = end = 0; module->cc[outNum].num = -1; - } else { + } + else { module->cc[outNum].num = num; module->cc[outNum].resetSync(); } - } catch (...) { + } + catch (...) { text = ""; begin = end = 0; module->cc[outNum].num = -1; @@ -309,7 +313,8 @@ MIDICCToCVWidget::MIDICCToCVWidget() { if ((i + 1) % 4 == 0) { yPos += 47 + margin; - } else { + } + else { yPos -= labelHeight + margin; } } diff --git a/src/core/MidiClockToCV.cpp b/src/core/MidiClockToCV.cpp index 8e3f5516..1b5186c7 100644 --- a/src/core/MidiClockToCV.cpp +++ b/src/core/MidiClockToCV.cpp @@ -1,10 +1,10 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" #include "dsp/digital.hpp" + using namespace rack; struct MIDIClockToCVInterface : MidiIO, Module { @@ -71,7 +71,7 @@ struct MIDIClockToCVInterface : MidiIO, Module { void resetMidi() override; - json_t *toJson() override{ + json_t *toJson() override { json_t *rootJ = json_object(); addBaseJson(rootJ); json_object_set_new(rootJ, "clock1ratio", json_integer(clock1ratio)); @@ -79,7 +79,7 @@ struct MIDIClockToCVInterface : MidiIO, Module { return rootJ; } - void fromJson(json_t *rootJ) override{ + void fromJson(json_t *rootJ) override { baseFromJson(rootJ); json_t *c1rJ = json_object_get(rootJ, "clock1ratio"); if (c1rJ) { @@ -101,9 +101,8 @@ void MIDIClockToCVInterface::step() { // midiIn->getMessage returns empty vector if there are no messages in the queue getMessage(&message); - while (message.size() > 0) { + if (message.size() > 0) { processMidi(message); - getMessage(&message); } } @@ -185,18 +184,18 @@ void MIDIClockToCVInterface::resetMidi() { void MIDIClockToCVInterface::processMidi(std::vector msg) { switch (msg[0]) { - case 0xfa: - start = true; - break; - case 0xfb: - cont = true; - break; - case 0xfc: - stop = true; - break; - case 0xf8: - tick = true; - break; + case 0xfa: + start = true; + break; + case 0xfb: + cont = true; + break; + case 0xfc: + stop = true; + break; + case 0xf8: + tick = true; + break; } @@ -218,13 +217,15 @@ struct ClockRatioItem : MenuItem { struct ClockRatioChoice : ChoiceButton { int *clockRatio; const std::vector ratioNames = {"Sixteenth note (1:4 ratio)", "Eighth note triplet (1:3 ratio)", - "Eighth note (1:2 ratio)", "Quarter note triplet (2:3 ratio)", - "Quarter note (tap speed)", "Half note triplet (4:3 ratio)", - "Half note (2:1 ratio)", "Whole note (4:1 ratio)", - "Two whole notes (8:1 ratio)"}; + "Eighth note (1:2 ratio)", "Quarter note triplet (2:3 ratio)", + "Quarter note (tap speed)", "Half note triplet (4:3 ratio)", + "Half note (2:1 ratio)", "Whole note (4:1 ratio)", + "Two whole notes (8:1 ratio)" + }; const std::vector ratioNames_short = {"1:4 ratio", "1:3 ratio", "1:2 ratio", "2:3 ratio", "1:1 ratio", - "4:3", "2:1 ratio", "4:1 ratio", "8:1 ratio"}; + "4:3", "2:1 ratio", "4:1 ratio", "8:1 ratio" + }; void onAction(EventAction &e) override { Menu *menu = gScene->createMenu(); @@ -236,7 +237,7 @@ struct ClockRatioChoice : ChoiceButton { clockRatioItem->ratio = ratio; clockRatioItem->clockRatio = clockRatio; clockRatioItem->text = ratioNames[ratio]; - menu->pushChild(clockRatioItem); + menu->addChild(clockRatioItem); } } @@ -270,7 +271,7 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); label->text = "MIDI Clk-CV"; addChild(label); - yPos = labelHeight*2; + yPos = labelHeight * 2; } { @@ -320,8 +321,8 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { addInput(createInput(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO)); ClockRatioChoice *ratioChoice = new ClockRatioChoice(); ratioChoice->clockRatio = &module->clock1ratio; - ratioChoice->box.pos = Vec(int(box.size.x/3), yPos); - ratioChoice->box.size.x = int(box.size.x/1.5 - margin); + ratioChoice->box.pos = Vec(int(box.size.x / 3), yPos); + ratioChoice->box.size.x = int(box.size.x / 1.5 - margin); addChild(ratioChoice); yPos += ratioChoice->box.size.y + margin * 3; @@ -344,8 +345,8 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { addInput(createInput(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_RATIO)); ClockRatioChoice *ratioChoice = new ClockRatioChoice(); ratioChoice->clockRatio = &module->clock2ratio; - ratioChoice->box.pos = Vec(int(box.size.x/3), yPos); - ratioChoice->box.size.x = int(box.size.x/1.5 - margin); + ratioChoice->box.pos = Vec(int(box.size.x / 3), yPos); + ratioChoice->box.size.x = int(box.size.x / 1.5 - margin); addChild(ratioChoice); yPos += ratioChoice->box.size.y + margin * 3; diff --git a/src/core/MidiIO.cpp b/src/core/MidiIO.cpp index 2ca166ee..74f71bfb 100644 --- a/src/core/MidiIO.cpp +++ b/src/core/MidiIO.cpp @@ -1,9 +1,9 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" + using namespace rack; @@ -59,7 +59,8 @@ std::vector MidiIO::getDevices() { RtMidiIn *m; try { m = new RtMidiIn(); - } catch (RtMidiError &error) { + } + catch (RtMidiError &error) { warn("Failed to create RtMidiIn: %s", error.getMessage().c_str()); return names; } @@ -209,7 +210,7 @@ void MidiChoice::onAction(EventAction &e) { MidiItem *midiItem = new MidiItem(); midiItem->midiModule = midiModule; midiItem->text = ""; - menu->pushChild(midiItem); + menu->addChild(midiItem); } std::vector deviceNames = midiModule->getDevices(); @@ -217,7 +218,7 @@ void MidiChoice::onAction(EventAction &e) { MidiItem *midiItem = new MidiItem(); midiItem->midiModule = midiModule; midiItem->text = deviceNames[i]; - menu->pushChild(midiItem); + menu->addChild(midiItem); } } @@ -245,14 +246,14 @@ void ChannelChoice::onAction(EventAction &e) { channelItem->midiModule = midiModule; channelItem->channel = -1; channelItem->text = "All"; - menu->pushChild(channelItem); + menu->addChild(channelItem); } for (int channel = 0; channel < 16; channel++) { ChannelItem *channelItem = new ChannelItem(); channelItem->midiModule = midiModule; channelItem->channel = channel; channelItem->text = stringf("%d", channel + 1); - menu->pushChild(channelItem); + menu->addChild(channelItem); } } diff --git a/src/core/MidiIO.hpp b/src/core/MidiIO.hpp index 5148c5fa..756822d6 100644 --- a/src/core/MidiIO.hpp +++ b/src/core/MidiIO.hpp @@ -1,10 +1,15 @@ #include #include "rack.hpp" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsuggest-override" #include "rtmidi/RtMidi.h" +#pragma GCC diagnostic pop using namespace rack; + struct IgnoreFlags { bool midiSysex = true; bool midiTime = true; @@ -16,7 +21,6 @@ struct MidiMessage { double timeStamp; MidiMessage() : bytes(0), timeStamp(0.0) {}; - }; /** @@ -131,10 +135,10 @@ struct TransitionSmoother { switch (m) { case DELTA: /* If the change is smaller, the transition phase is longer */ - this->step = delta > 0 ? delta/l : -delta/l; + this->step = delta > 0 ? delta / l : -delta / l; break; case CONST: - this->step = 1.0/l; + this->step = 1.0 / l; break; } @@ -149,13 +153,13 @@ struct TransitionSmoother { switch (t) { case SMOOTHSTEP: - next += delta*x*x*(3-2*x); + next += delta * x * x * (3 - 2 * x); break; case EXP: - next += delta*x*x; + next += delta * x * x; break; case LIN: - next += delta*x; + next += delta * x; break; } diff --git a/src/core/MidiToCV.cpp b/src/core/MidiToCV.cpp index 69be33be..df8accd9 100644 --- a/src/core/MidiToCV.cpp +++ b/src/core/MidiToCV.cpp @@ -1,10 +1,10 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" #include "dsp/digital.hpp" + /* * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to * CV @@ -74,7 +74,7 @@ struct MIDIToCVInterface : MidiIO, Module { baseFromJson(rootJ); } - void reset() override { + void onReset() override { resetMidi(); } @@ -100,9 +100,8 @@ void MIDIToCVInterface::step() { // midiIn->getMessage returns empty vector if there are no messages in the queue getMessage(&message); - while (message.size() > 0) { + if (message.size() > 0) { processMidi(message); - getMessage(&message); } } diff --git a/src/core/MidiTriggerToCV.cpp b/src/core/MidiTriggerToCV.cpp index 4bb60020..3fb0f1c1 100644 --- a/src/core/MidiTriggerToCV.cpp +++ b/src/core/MidiTriggerToCV.cpp @@ -1,10 +1,10 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" #include "dsp/digital.hpp" + using namespace rack; struct TriggerValue { @@ -63,7 +63,7 @@ struct MIDITriggerToCVInterface : MidiIO, Module { } } - void reset() override { + void onReset() override { resetMidi(); } }; @@ -75,9 +75,8 @@ void MIDITriggerToCVInterface::step() { // midiIn->getMessage returns empty vector if there are no messages in the queue getMessage(&message); - while (message.size() > 0) { + if (message.size() > 0) { processMidi(message); - getMessage(&message); } } @@ -110,7 +109,7 @@ void MIDITriggerToCVInterface::processMidi(std::vector msg) { if (status == 0x8) { // note off for (int i = 0; i < NUM_OUTPUTS; i++) { if (data1 == trigger[i].num) { - trigger[i].val = data2; + trigger[i].val = 0; } } return; @@ -169,10 +168,12 @@ void TriggerTextField::onTextChange() { text = ""; begin = end = 0; module->trigger[outNum].num = -1; - }else { + } + else { module->trigger[outNum].num = num; } - } catch (...) { + } + catch (...) { text = ""; begin = end = 0; module->trigger[outNum].num = -1; @@ -271,7 +272,8 @@ MIDITriggerToCVWidget::MIDITriggerToCVWidget() { if ((i + 1) % 4 == 0) { yPos += 47 + margin; - } else { + } + else { yPos -= labelHeight + margin; } } diff --git a/src/core/QuadMidiToCV.cpp b/src/core/QuadMidiToCV.cpp index 39982913..84c7335b 100644 --- a/src/core/QuadMidiToCV.cpp +++ b/src/core/QuadMidiToCV.cpp @@ -1,15 +1,16 @@ #include #include -#include "rtmidi/RtMidi.h" #include "core.hpp" #include "MidiIO.hpp" #include "dsp/digital.hpp" + struct MidiKey { int pitch = 60; int at = 0; // aftertouch int vel = 0; // velocity bool gate = false; + bool pedal_gate_released = false; }; struct QuadMIDIToCVInterface : MidiIO, Module { @@ -72,7 +73,7 @@ struct QuadMIDIToCVInterface : MidiIO, Module { baseFromJson(rootJ); } - void reset() override { + void onReset() override { resetMidi(); } @@ -98,16 +99,11 @@ void QuadMIDIToCVInterface::resetMidi() { void QuadMIDIToCVInterface::step() { if (isPortOpen()) { std::vector message; - int msgsProcessed = 0; // midiIn->getMessage returns empty vector if there are no messages in the queue - // NOTE: For the quadmidi we will process max 4 midi messages per step to avoid - // problems with parallel input. getMessage(&message); - while (msgsProcessed < 4 && message.size() > 0) { + if (message.size() > 0) { processMidi(message); - getMessage(&message); - msgsProcessed++; } } @@ -140,49 +136,60 @@ void QuadMIDIToCVInterface::processMidi(std::vector msg) { return; switch (status) { - // note off - case 0x8: { + // note off + case 0x8: { + gate = false; + } + break; + case 0x9: // note on + if (data2 > 0) { + gate = true; + } + else { + // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. gate = false; } - break; - case 0x9: // note on - if (data2 > 0) { - gate = true; - } else { - // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. - gate = false; - } - break; - case 0xa: // channel aftertouch - for (int i = 0; i < 4; i++) { - if (activeKeys[i].pitch == data1) { - activeKeys[i].at = data2; - } + break; + case 0xa: // channel aftertouch + for (int i = 0; i < 4; i++) { + if (activeKeys[i].pitch == data1) { + activeKeys[i].at = data2; } - return; - case 0xb: // cc - if (data1 == 0x40) { // pedal - pedal = (data2 >= 64); - if (!pedal) { - open.clear(); - for (int i = 0; i < 4; i++) { + } + return; + case 0xb: // cc + if (data1 == 0x40) { // pedal + pedal = (data2 >= 64); + if (!pedal) { + for (int i = 0; i < 4; i++) { + if (activeKeys[i].pedal_gate_released) { activeKeys[i].gate = false; - open.push_back(i); + activeKeys[i].pedal_gate_released = false; + if (std::find(open.begin(), open.end(), i) != open.end()) { + open.remove(i); + } + open.push_front(i); } } } - return; - default: - return; + } + return; + default: + return; } if (pedal && !gate) { + for (int i = 0; i < 4; i++) { + if (activeKeys[i].pitch == data1 && activeKeys[i].gate) { + activeKeys[i].pedal_gate_released = true; + } + } return; } if (!gate) { for (int i = 0; i < 4; i++) { - if (activeKeys[i].pitch == data1) { + if (activeKeys[i].pitch == data1 && activeKeys[i].gate) { activeKeys[i].gate = false; activeKeys[i].vel = data2; if (std::find(open.begin(), open.end(), i) != open.end()) { @@ -201,25 +208,26 @@ void QuadMIDIToCVInterface::processMidi(std::vector msg) { } if (!activeKeys[0].gate && !activeKeys[1].gate && - !activeKeys[2].gate && !activeKeys[3].gate) { + !activeKeys[2].gate && !activeKeys[3].gate) { open.sort(); } switch (mode) { - case RESET: - if (open.size() >= 4) { - for (int i = 0; i < 4; i++) { - activeKeys[i].gate = false; - open.push_back(i); - } + case RESET: + if (open.size() >= 4) { + open.clear(); + for (int i = 0; i < 4; i++) { + activeKeys[i].gate = false; + open.push_back(i); } - break; - case REASSIGN: - open.push_back(open.front()); - break; - case ROTATE: - break; + } + break; + case REASSIGN: + open.push_back(open.front()); + break; + case ROTATE: + break; } int next = open.front(); @@ -233,10 +241,12 @@ void QuadMIDIToCVInterface::processMidi(std::vector msg) { open.push_front(i); activeKeys[i].gate = false; + activeKeys[i].pedal_gate_released = false; } } activeKeys[next].gate = true; + activeKeys[next].pedal_gate_released = false; activeKeys[next].pitch = data1; activeKeys[next].vel = data2; } @@ -254,7 +264,7 @@ struct ModeItem : MenuItem { int mode; QuadMIDIToCVInterface *module; - void onAction(EventAction &e) { + void onAction(EventAction &e) override { module->setMode(mode); } }; @@ -264,7 +274,7 @@ struct ModeChoice : ChoiceButton { const std::vector modeNames = {"ROTATE", "RESET", "REASSIGN"}; - void onAction(EventAction &e) { + void onAction(EventAction &e) override { Menu *menu = gScene->createMenu(); menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)).round(); menu->box.size.x = box.size.x; @@ -274,11 +284,11 @@ struct ModeChoice : ChoiceButton { modeItem->mode = i; modeItem->module = module; modeItem->text = modeNames[i]; - menu->pushChild(modeItem); + menu->addChild(modeItem); } } - void step() { + void step() override { text = modeNames[module->getMode()]; } }; @@ -313,7 +323,7 @@ QuadMidiToCVWidget::QuadMidiToCVWidget() { } addParam(createParam(Vec(12 * 15, labelHeight), module, QuadMIDIToCVInterface::RESET_PARAM, 0.0, 1.0, - 0.0)); + 0.0)); addChild(createLight>(Vec(12 * 15 + 5, labelHeight + 5), module, QuadMIDIToCVInterface::RESET_LIGHT)); { Label *label = new Label(); diff --git a/src/gui.cpp b/src/gui.cpp index af9632fd..8b418c40 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -8,8 +8,10 @@ #include "../ext/osdialog/osdialog.h" -#define NANOVG_GL2_IMPLEMENTATION -// #define NANOVG_GL3_IMPLEMENTATION +#define NANOVG_GL2 1 +// #define NANOVG_GL3 1 +// #define NANOVG_GLES2 1 +#define NANOVG_GL_IMPLEMENTATION 1 #include "../ext/nanovg/src/nanovg_gl.h" // Hack to get framebuffer objects working on OpenGL 2 (we blindly assume the extension is supported) #define NANOVG_FBO_VALID 1 @@ -31,7 +33,8 @@ GLFWwindow *gWindow = NULL; NVGcontext *gVg = NULL; NVGcontext *gFramebufferVg = NULL; std::shared_ptr gGuiFont; -float gPixelRatio = 0.0; +float gPixelRatio = 1.0; +float gWindowRatio = 1.0; bool gAllowCursorLock = true; int gGuiFrame; Vec gMousePos; @@ -40,7 +43,6 @@ std::string lastWindowTitle; void windowSizeCallback(GLFWwindow* window, int width, int height) { - gScene->box.size = Vec(width, height); } void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods) { @@ -143,14 +145,16 @@ void mouseButtonStickyCallback(GLFWwindow *window, int button, int action, int m } void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) { - Vec mousePos = Vec(xpos, ypos).round(); + Vec mousePos = Vec(xpos, ypos).div(gPixelRatio / gWindowRatio).round(); Vec mouseRel = mousePos.minus(gMousePos); + int cursorMode = glfwGetInputMode(gWindow, GLFW_CURSOR); + (void) cursorMode; + #ifdef ARCH_MAC // Workaround for Mac. We can't use GLFW_CURSOR_DISABLED because it's buggy, so implement it on our own. // This is not an ideal implementation. For example, if the user drags off the screen, the new mouse position will be clamped. - int mouseMode = glfwGetInputMode(gWindow, GLFW_CURSOR); - if (mouseMode == GLFW_CURSOR_HIDDEN) { + if (cursorMode == GLFW_CURSOR_HIDDEN) { // CGSetLocalEventsSuppressionInterval(0.0); glfwSetCursorPos(gWindow, gMousePos.x, gMousePos.y); CGAssociateMouseAndMouseCursorPosition(true); @@ -311,12 +315,15 @@ void guiInit() { exit(1); } +#if defined NANOVG_GL2 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); - // glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - // glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); - // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); - // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#elif defined NANOVG_GL3 + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#endif glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE); lastWindowTitle = ""; @@ -332,6 +339,7 @@ void guiInit() { glfwSetWindowSizeCallback(gWindow, windowSizeCallback); glfwSetMouseButtonCallback(gWindow, mouseButtonStickyCallback); + // Call this ourselves, but on every frame instead of only when the mouse moves // glfwSetCursorPosCallback(gWindow, cursorPosCallback); glfwSetCursorEnterCallback(gWindow, cursorEnterCallback); glfwSetScrollCallback(gWindow, scrollCallback); @@ -353,11 +361,22 @@ void guiInit() { glfwSetWindowSizeLimits(gWindow, 640, 480, GLFW_DONT_CARE, GLFW_DONT_CARE); // Set up NanoVG +#if defined NANOVG_GL2 gVg = nvgCreateGL2(NVG_ANTIALIAS); - // gVg = nvgCreateGL3(NVG_ANTIALIAS); +#elif defined NANOVG_GL3 + gVg = nvgCreateGL3(NVG_ANTIALIAS); +#elif defined NANOVG_GLES2 + gVg = nvgCreateGLES2(NVG_ANTIALIAS); +#endif assert(gVg); +#if defined NANOVG_GL2 gFramebufferVg = nvgCreateGL2(NVG_ANTIALIAS); +#elif defined NANOVG_GL3 + gFramebufferVg = nvgCreateGL3(NVG_ANTIALIAS); +#elif defined NANOVG_GLES2 + gFramebufferVg = nvgCreateGLES2(NVG_ANTIALIAS); +#endif assert(gFramebufferVg); // Set up Blendish @@ -375,20 +394,29 @@ void guiInit() { void guiDestroy() { gGuiFont.reset(); + +#if defined NANOVG_GL2 nvgDeleteGL2(gVg); - // nvgDeleteGL3(gVg); +#elif defined NANOVG_GL3 + nvgDeleteGL3(gVg); +#elif defined NANOVG_GLES2 + nvgDeleteGLES2(gVg); +#endif + +#if defined NANOVG_GL2 nvgDeleteGL2(gFramebufferVg); +#elif defined NANOVG_GL3 + nvgDeleteGL3(gFramebufferVg); +#elif defined NANOVG_GLES2 + nvgDeleteGLES2(gFramebufferVg); +#endif + glfwDestroyWindow(gWindow); glfwTerminate(); } void guiRun() { assert(gWindow); - { - int width, height; - glfwGetWindowSize(gWindow, &width, &height); - windowSizeCallback(gWindow, width, height); - } gGuiFrame = 0; while(!glfwWindowShouldClose(gWindow)) { double startTime = glfwGetTime(); @@ -417,18 +445,25 @@ void guiRun() { lastWindowTitle = windowTitle; } - // Get framebuffer size - int width, height; - glfwGetFramebufferSize(gWindow, &width, &height); - int windowWidth, windowHeight; - glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); - float pixelRatio = (float)width / windowWidth; + // Get desired scaling + float pixelRatio; + glfwGetWindowContentScale(gWindow, &pixelRatio, NULL); + pixelRatio = roundf(pixelRatio); if (pixelRatio != gPixelRatio) { EventZoom eZoom; gScene->onZoom(eZoom); gPixelRatio = pixelRatio; } + // Get framebuffer/window ratio + int width, height; + glfwGetFramebufferSize(gWindow, &width, &height); + int windowWidth, windowHeight; + glfwGetWindowSize(gWindow, &windowWidth, &windowHeight); + gWindowRatio = (float)width / windowWidth; + + gScene->box.size = Vec(width, height).div(gPixelRatio / gWindowRatio); + // Step scene gScene->step(); diff --git a/src/main.cpp b/src/main.cpp index d8a45214..bc32af17 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,13 +35,7 @@ int main(int argc, char* argv[]) { engineInit(); guiInit(); sceneInit(); - if (argc >= 2) { - // TODO Set gRackWidget->lastPath - gRackWidget->loadPatch(argv[1]); - } - else { - gRackWidget->loadPatch(assetLocal("autosave.vcv")); - } + gRackWidget->loadPatch(assetLocal("autosave.vcv")); settingsLoad(assetLocal("settings.json")); engineStart(); diff --git a/src/plugin.cpp b/src/plugin.cpp index ad7a868c..8c5d0b41 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -7,11 +7,12 @@ #include #include // for MAXPATHLEN #include +#include #include #include -#if ARCH_WIN +#if defined(ARCH_WIN) #include #include #define mkdir(_dir, _perms) _mkdir(_dir) @@ -31,52 +32,6 @@ namespace rack { std::list gPlugins; std::string gToken; -std::string gTagNames[NUM_TAGS] = { - "Amplifier/VCA", - "Attenuator", - "Blank", - "Clock", - "Controller", - "Delay", - "Digital", - "Distortion", - "Drum", - "Dual/Stereo", - "Dynamics", - "Effect", - "Envelope Follower", - "Envelope Generator", - "Equalizer", - "External", - "Filter/VCF", - "Function Generator", - "Granular", - "LFO", - "Logic", - "Low Pass Gate", - "MIDI", - "Mixer", - "Multiple", - "Noise", - "Oscillator/VCO", - "Panning", - "Quad", - "Quantizer", - "Random", - "Reverb", - "Ring Modulator", - "Sample and Hold", - "Sampler", - "Sequencer", - "Slew Limiter", - "Switch", - "Synth Voice", - "Tuner", - "Utility", - "Visual", - "Waveshaper", -}; - static bool isDownloading = false; static float downloadProgress = 0.0; @@ -109,7 +64,9 @@ static int loadPlugin(std::string path) { // Load dynamic/shared library #if ARCH_WIN + SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); HINSTANCE handle = LoadLibrary(libraryFilename.c_str()); + SetErrorMode(0); if (!handle) { int error = GetLastError(); warn("Failed to load library %s: %d", libraryFilename.c_str(), error); @@ -142,9 +99,20 @@ static int loadPlugin(std::string path) { plugin->handle = handle; initCallback(plugin); + // Reject plugin if slug already exists + for (Plugin *p : gPlugins) { + if (plugin->slug == p->slug) { + warn("Plugin \"%s\" is already loaded, not attempting to load it again"); + // TODO + // Fix memory leak with `plugin` here + return -1; + } + } + // Add plugin to list gPlugins.push_back(plugin); info("Loaded plugin %s", libraryFilename.c_str()); + return 0; } @@ -222,24 +190,55 @@ static int extractZip(const char *filename, const char *dir) { return err; } -static void refreshPurchase(json_t *pluginJ) { +static void syncPlugin(json_t *pluginJ) { json_t *slugJ = json_object_get(pluginJ, "slug"); if (!slugJ) return; - const char *slug = json_string_value(slugJ); + std::string slug = json_string_value(slugJ); + info("Syncing plugin %s", slug.c_str()); json_t *nameJ = json_object_get(pluginJ, "name"); if (!nameJ) return; - const char *name = json_string_value(nameJ); - - // Append token and version to download URL - std::string url = gApiHost; - url += "/download"; - url += "?product="; - url += slug; - url += "&version="; - url += requestEscape(gApplicationVersion); - url += "&token="; - url += requestEscape(gToken); + std::string name = json_string_value(nameJ); + + std::string download; + std::string sha256; + + json_t *downloadsJ = json_object_get(pluginJ, "downloads"); + if (downloadsJ) { +#if defined(ARCH_WIN) + #define DOWNLOADS_ARCH "win" +#elif defined(ARCH_MAC) + #define DOWNLOADS_ARCH "mac" +#elif defined(ARCH_LIN) + #define DOWNLOADS_ARCH "lin" +#endif + json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); + if (archJ) { + // Get download URL + json_t *downloadJ = json_object_get(archJ, "download"); + if (downloadJ) + download = json_string_value(downloadJ); + // Get SHA256 hash + json_t *sha256J = json_object_get(archJ, "sha256"); + if (sha256J) + sha256 = json_string_value(sha256J); + } + } + + json_t *productIdJ = json_object_get(pluginJ, "productId"); + if (productIdJ) { + download = gApiHost; + download += "/download"; + download += "?slug="; + download += slug; + download += "&token="; + download += requestEscape(gToken); + } + + if (download.empty()) { + warn("Could not get download URL for plugin %s", slug.c_str()); + return; + } // If plugin is not loaded, download the zip file to /plugins downloadName = name; @@ -249,26 +248,133 @@ static void refreshPurchase(json_t *pluginJ) { std::string pluginsDir = assetLocal("plugins"); std::string pluginPath = pluginsDir + "/" + slug; std::string zipPath = pluginPath + ".zip"; - bool success = requestDownload(url, zipPath, &downloadProgress); + bool success = requestDownload(download, zipPath, &downloadProgress); if (success) { + if (!sha256.empty()) { + // Check SHA256 hash + std::string actualSha256 = requestSHA256File(zipPath); + if (actualSha256 != sha256) { + warn("Plugin %s does not match expected SHA256 checksum", slug.c_str()); + return; + } + } + // Unzip file int err = extractZip(zipPath.c_str(), pluginsDir.c_str()); if (!err) { // Delete zip remove(zipPath.c_str()); // Load plugin - loadPlugin(pluginPath); + // loadPlugin(pluginPath); } } downloadName = ""; } +static bool trySyncPlugin(json_t *pluginJ, json_t *communityPluginsJ, bool dryRun) { + std::string slug = json_string_value(pluginJ); + + // Find community plugin + size_t communityIndex; + json_t *communityPluginJ = NULL; + json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) { + json_t *communitySlugJ = json_object_get(communityPluginJ, "slug"); + if (communitySlugJ) { + std::string communitySlug = json_string_value(communitySlugJ); + if (slug == communitySlug) + break; + } + } + if (communityIndex == json_array_size(communityPluginsJ)) { + warn("Plugin sync error: %s not found in community", slug.c_str()); + return false; + } + + // Get community version + std::string version; + json_t *versionJ = json_object_get(communityPluginJ, "version"); + if (versionJ) { + version = json_string_value(versionJ); + } + + // Check whether we already have a plugin with the same slug and version + for (Plugin *plugin : gPlugins) { + if (plugin->slug == slug) { + // plugin->version might be blank, so adding a version of the manifest will update the plugin + if (plugin->version == version) + return false; + } + } + + if (!dryRun) + syncPlugin(communityPluginJ); + return true; +} + +bool pluginSync(bool dryRun) { + if (gToken.empty()) + return false; + + bool available = false; + + // Download my plugins + json_t *reqJ = json_object(); + json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str())); + json_object_set(reqJ, "token", json_string(gToken.c_str())); + json_t *resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ); + json_decref(reqJ); + + // Download community plugins + json_t *communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL); + + if (!dryRun) { + isDownloading = true; + downloadProgress = 0.0; + downloadName = ""; + } + + if (resJ && communityResJ) { + json_t *errorJ = json_object_get(resJ, "error"); + json_t *communityErrorJ = json_object_get(resJ, "error"); + if (errorJ) { + warn("Plugin sync error: %s", json_string_value(errorJ)); + } + else if (communityErrorJ) { + warn("Plugin sync error: %s", json_string_value(communityErrorJ)); + } + else { + // Check each plugin in list of my plugins + json_t *pluginsJ = json_object_get(resJ, "plugins"); + json_t *communityPluginsJ = json_object_get(communityResJ, "plugins"); + size_t index; + json_t *pluginJ; + json_array_foreach(pluginsJ, index, pluginJ) { + if (trySyncPlugin(pluginJ, communityPluginsJ, dryRun)) + available = true; + } + } + } + + if (resJ) + json_decref(resJ); + + if (communityResJ) + json_decref(communityResJ); + + if (!dryRun) { + isDownloading = false; + } + + return available; +} + //////////////////// // plugin API //////////////////// void pluginInit() { + tagsInit(); // Load core // This function is defined in core.cpp Plugin *coreManufacturer = new Plugin(); @@ -292,10 +398,10 @@ void pluginInit() { void pluginDestroy() { for (Plugin *plugin : gPlugins) { // Free library handle -#if ARCH_WIN +#if defined(ARCH_WIN) if (plugin->handle) FreeLibrary((HINSTANCE)plugin->handle); -#elif ARCH_LIN || ARCH_MAC +#elif defined(ARCH_LIN) || defined(ARCH_MAC) if (plugin->handle) dlclose(plugin->handle); #endif @@ -336,39 +442,6 @@ void pluginLogOut() { gToken = ""; } -void pluginRefresh() { - if (gToken.empty()) - return; - - isDownloading = true; - downloadProgress = 0.0; - downloadName = ""; - - json_t *reqJ = json_object(); - json_object_set(reqJ, "token", json_string(gToken.c_str())); - json_t *resJ = requestJson(METHOD_GET, gApiHost + "/purchases", reqJ); - json_decref(reqJ); - - if (resJ) { - json_t *errorJ = json_object_get(resJ, "error"); - if (errorJ) { - const char *errorStr = json_string_value(errorJ); - warn("Plugin refresh error: %s", errorStr); - } - else { - json_t *purchasesJ = json_object_get(resJ, "purchases"); - size_t index; - json_t *purchaseJ; - json_array_foreach(purchasesJ, index, purchaseJ) { - refreshPurchase(purchaseJ); - } - } - json_decref(resJ); - } - - isDownloading = false; -} - void pluginCancelDownload() { // TODO } diff --git a/src/tags.cpp b/src/tags.cpp new file mode 100644 index 00000000..49302164 --- /dev/null +++ b/src/tags.cpp @@ -0,0 +1,64 @@ +#include "tags.hpp" + + +namespace rack { + + +std::string gTagNames[NUM_TAGS]; + + +void tagsInit() { + gTagNames[AMPLIFIER_TAG] = "Amplifier/VCA"; + gTagNames[ATTENUATOR_TAG] = "Attenuator"; + gTagNames[BLANK_TAG] = "Blank"; + gTagNames[CHORUS_TAG] = "Chorus"; + gTagNames[CLOCK_TAG] = "Clock"; + gTagNames[COMPRESSOR_TAG] = "Compressor"; + gTagNames[CONTROLLER_TAG] = "Controller"; + gTagNames[DELAY_TAG] = "Delay"; + gTagNames[DIGITAL_TAG] = "Digital"; + gTagNames[DISTORTION_TAG] = "Distortion"; + gTagNames[DRUM_TAG] = "Drum"; + gTagNames[DUAL_TAG] = "Dual/Stereo"; + gTagNames[DYNAMICS_TAG] = "Dynamics"; + gTagNames[EFFECT_TAG] = "Effect"; + gTagNames[ENVELOPE_FOLLOWER_TAG] = "Envelope Follower"; + gTagNames[ENVELOPE_GENERATOR_TAG] = "Envelope Generator"; + gTagNames[EQUALIZER_TAG] = "Equalizer"; + gTagNames[EXTERNAL_TAG] = "External"; + gTagNames[FILTER_TAG] = "Filter/VCF"; + gTagNames[FLANGER_TAG] = "Flanger"; + gTagNames[FUNCTION_GENERATOR_TAG] = "Function Generator"; + gTagNames[GRANULAR_TAG] = "Granular"; + gTagNames[LFO_TAG] = "LFO"; + gTagNames[LIMITER_TAG] = "Limiter"; + gTagNames[LOGIC_TAG] = "Logic"; + gTagNames[LOW_PASS_GATE_TAG] = "Low Pass Gate"; + gTagNames[MIDI_TAG] = "MIDI"; + gTagNames[MIXER_TAG] = "Mixer"; + gTagNames[MULTIPLE_TAG] = "Multiple"; + gTagNames[NOISE_TAG] = "Noise"; + gTagNames[OSCILLATOR_TAG] = "Oscillator/VCO"; + gTagNames[PANNING_TAG] = "Panning"; + gTagNames[PHASER_TAG] = "Phaser"; + gTagNames[QUAD_TAG] = "Quad"; + gTagNames[QUANTIZER_TAG] = "Quantizer"; + gTagNames[RANDOM_TAG] = "Random"; + gTagNames[RECORDING_TAG] = "Recording"; + gTagNames[REVERB_TAG] = "Reverb"; + gTagNames[RING_MODULATOR_TAG] = "Ring Modulator"; + gTagNames[SAMPLE_AND_HOLD_TAG] = "Sample and Hold"; + gTagNames[SAMPLER_TAG] = "Sampler"; + gTagNames[SEQUENCER_TAG] = "Sequencer"; + gTagNames[SLEW_LIMITER_TAG] = "Slew Limiter"; + gTagNames[SWITCH_TAG] = "Switch"; + gTagNames[SYNTH_VOICE_TAG] = "Synth Voice"; + gTagNames[TUNER_TAG] = "Tuner"; + gTagNames[UTILITY_TAG] = "Utility"; + gTagNames[VISUAL_TAG] = "Visual"; + gTagNames[VOCODER_TAG] = "Vocoder"; + gTagNames[WAVESHAPER_TAG] = "Waveshaper"; +} + + +} // namespace rack diff --git a/src/util.cpp b/src/util.cpp index e01c10a9..7417af1f 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -167,6 +167,7 @@ void debug(const char *format, ...) { fprintf(gLogFile, "[debug] "); vfprintf(gLogFile, format, args); fprintf(gLogFile, "\n"); + fflush(gLogFile); va_end(args); } @@ -176,6 +177,7 @@ void info(const char *format, ...) { fprintf(gLogFile, "[info] "); vfprintf(gLogFile, format, args); fprintf(gLogFile, "\n"); + fflush(gLogFile); va_end(args); } @@ -185,6 +187,7 @@ void warn(const char *format, ...) { fprintf(gLogFile, "[warning] "); vfprintf(gLogFile, format, args); fprintf(gLogFile, "\n"); + fflush(gLogFile); va_end(args); } @@ -194,6 +197,7 @@ void fatal(const char *format, ...) { fprintf(gLogFile, "[fatal] "); vfprintf(gLogFile, format, args); fprintf(gLogFile, "\n"); + fflush(gLogFile); va_end(args); } diff --git a/src/util/request.cpp b/src/util/request.cpp index c046bbb7..72702c04 100644 --- a/src/util/request.cpp +++ b/src/util/request.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace rack { @@ -79,6 +80,7 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ) { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, reqStr); std::string resText; + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resText); @@ -130,6 +132,8 @@ bool requestDownload(std::string url, std::string filename, float *progress) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); info("Downloading %s", url.c_str()); CURLcode res = curl_easy_perform(curl); @@ -153,5 +157,36 @@ std::string requestEscape(std::string s) { return ret; } +std::string requestSHA256File(std::string filename) { + FILE *f = fopen(filename.c_str(), "rb"); + if (!f) + return ""; + + uint8_t hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha256; + SHA256_Init(&sha256); + const int bufferLen = 1 << 15; + uint8_t *buffer = new uint8_t[bufferLen]; + int len = 0; + while ((len = fread(buffer, 1, bufferLen, f))) { + SHA256_Update(&sha256, buffer, len); + } + SHA256_Final(hash, &sha256); + delete[] buffer; + fclose(f); + + // Convert binary hash to hex + char hashHex[64]; + const char hexTable[] = "0123456789abcdef"; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { + uint8_t h = hash[i]; + hashHex[2*i + 0] = hexTable[h >> 4]; + hashHex[2*i + 1] = hexTable[h & 0x0f]; + } + + std::string str(hashHex, sizeof(hashHex)); + return str; +} + } // namespace rack diff --git a/src/widgets/MenuOverlay.cpp b/src/widgets/MenuOverlay.cpp index efecef8c..85665b66 100644 --- a/src/widgets/MenuOverlay.cpp +++ b/src/widgets/MenuOverlay.cpp @@ -13,10 +13,12 @@ void MenuOverlay::step() { } } -void MenuOverlay::onDragDrop(EventDragDrop &e) { - if (e.origin == this) { +void MenuOverlay::onMouseDown(EventMouseDown &e) { + Widget::onMouseDown(e); + if (!e.consumed) { // deletes `this` gScene->setOverlay(NULL); + e.consumed = true; } } diff --git a/src/widgets/SVGWidget.cpp b/src/widgets/SVGWidget.cpp index 52b503e5..10138fc2 100644 --- a/src/widgets/SVGWidget.cpp +++ b/src/widgets/SVGWidget.cpp @@ -59,16 +59,19 @@ static void drawSVG(NVGcontext *vg, NSVGimage *svg) { for (NSVGshape *shape = svg->shapes; shape; shape = shape->next, shapeIndex++) { DEBUG_ONLY(printf(" new shape: %d id \"%s\", fillrule %d, from (%f, %f) to (%f, %f)\n", shapeIndex, shape->id, shape->fillRule, shape->bounds[0], shape->bounds[1], shape->bounds[2], shape->bounds[3]);) + // Visibility if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; nvgSave(vg); + // Opacity if (shape->opacity < 1.0) nvgGlobalAlpha(vg, shape->opacity); // Build path nvgBeginPath(vg); + // Iterate path linked list for (NSVGpath *path = shape->paths; path; path = path->next) { DEBUG_ONLY(printf(" new path: %d points, %s, from (%f, %f) to (%f, %f)\n", path->npts, path->closed ? "closed" : "open", path->bounds[0], path->bounds[1], path->bounds[2], path->bounds[3]);) @@ -81,6 +84,7 @@ static void drawSVG(NVGcontext *vg, NSVGimage *svg) { DEBUG_ONLY(printf(" bezier (%f, %f) to (%f, %f)\n", p[-2], p[-1], p[4], p[5]);) } + // Close path if (path->closed) nvgClosePath(vg); @@ -161,11 +165,12 @@ static void drawSVG(NVGcontext *vg, NSVGimage *svg) { } // Stroke shape - nvgStrokeWidth(vg, shape->strokeWidth); - // strokeDashOffset, strokeDashArray, strokeDashCount not yet supported - // strokeLineJoin, strokeLineCap not yet supported - if (shape->stroke.type) { + nvgStrokeWidth(vg, shape->strokeWidth); + // strokeDashOffset, strokeDashArray, strokeDashCount not yet supported + nvgLineCap(vg, (NVGlineCap) shape->strokeLineCap); + nvgLineJoin(vg, (int) shape->strokeLineJoin); + switch (shape->stroke.type) { case NSVG_PAINT_COLOR: { NVGcolor color = getNVGColor(shape->stroke.color); diff --git a/src/widgets/ZoomWidget.cpp b/src/widgets/ZoomWidget.cpp index c6d6cb5d..5ef4d335 100644 --- a/src/widgets/ZoomWidget.cpp +++ b/src/widgets/ZoomWidget.cpp @@ -65,5 +65,12 @@ void ZoomWidget::onScroll(EventScroll &e) { e.pos = pos; } +void ZoomWidget::onPathDrop(EventPathDrop &e) { + Vec pos = e.pos; + e.pos = e.pos.div(zoom); + Widget::onPathDrop(e); + e.pos = pos; +} + } // namespace rack