From 23cc7da9a64ceca23457d101ac2c53bc0c3d642a Mon Sep 17 00:00:00 2001 From: Patrick Desaulniers Date: Mon, 29 Apr 2019 19:34:03 -0400 Subject: [PATCH] Finish basic implementation of SVG support + MidiKeyboard example --- Makefile | 6 +- dgl/SVG.hpp | 17 +- dgl/src/Image.cpp | 5 +- dgl/src/SVG.cpp | 43 +- examples/MidiKeyboard/DistrhoPluginInfo.h | 3 +- examples/MidiKeyboard/KeyboardWidget.hpp | 477 +++++++++++++++++- examples/MidiKeyboard/Makefile | 9 +- .../MidiKeyboard/MidiKeyboardExampleUI.cpp | 52 +- .../resources/MidiKeyboardResources.hpp | 8 - 9 files changed, 546 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index f325a8a3..5b300fd8 100644 --- a/Makefile +++ b/Makefile @@ -22,9 +22,9 @@ examples: dgl $(MAKE) all -C examples/States $(MAKE) all -C examples/MidiKeyboard -#ifeq ($(HAVE_CAIRO),true) -# $(MAKE) all -C examples/CairoUI -#endif +ifeq ($(HAVE_CAIRO),true) + $(MAKE) all -C examples/CairoUI +endif ifneq ($(MACOS_OR_WINDOWS),true) # ExternalUI is WIP diff --git a/dgl/SVG.hpp b/dgl/SVG.hpp index 04988848..c69c29d1 100644 --- a/dgl/SVG.hpp +++ b/dgl/SVG.hpp @@ -36,16 +36,19 @@ public: */ SVG(); - /** - Constructor using raw SVG data. - */ - SVG(const char* const data, const uint width, const uint height, const float scaling = 1.0f); - /** Destructor. */ ~SVG(); + /** + Load SVG data from memory. + @note @a rawData must remain valid for the lifetime of this SVG. + */ + void loadFromMemory(const char* const rawData, + const uint dataSize, + const float scaling = 1.0f) noexcept; + /** Check if this SVG is valid. */ @@ -63,9 +66,9 @@ public: private: Size fSize; - NSVGrasterizer* fRasterizer; - NSVGimage* fImage; unsigned char* fRGBAData; + + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SVG) }; END_NAMESPACE_DGL diff --git a/dgl/src/Image.cpp b/dgl/src/Image.cpp index 70cd95a3..c34216c2 100644 --- a/dgl/src/Image.cpp +++ b/dgl/src/Image.cpp @@ -94,6 +94,8 @@ void Image::loadFromMemory(const char* const rawData, void Image::loadFromSVG(const SVG& svg) noexcept { + DISTRHO_SAFE_ASSERT_RETURN(svg.isValid(),) + loadFromMemory((const char*)svg.getRGBAData(), svg.getSize(), GL_RGBA); } @@ -119,8 +121,7 @@ Image& Image::operator=(const Image& image) noexcept void Image::_drawAt(const Point& pos) { - if (fTextureId == 0 || ! isValid()) - return; + DISTRHO_SAFE_ASSERT_RETURN(fTextureId != 0 && isValid(), ) glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, fTextureId); diff --git a/dgl/src/SVG.cpp b/dgl/src/SVG.cpp index d5d06cf7..776db509 100644 --- a/dgl/src/SVG.cpp +++ b/dgl/src/SVG.cpp @@ -27,45 +27,46 @@ START_NAMESPACE_DGL SVG::SVG() : fSize(), - fRasterizer(nullptr), - fImage(nullptr), fRGBAData(nullptr) { - } -SVG::SVG(const char* const data, const uint width, const uint height, const float scaling) - : fSize(width * scaling, height * scaling) +void SVG::loadFromMemory(const char* const rawData, const uint dataSize, const float scaling) noexcept { - DISTRHO_SAFE_ASSERT_RETURN(data != nullptr,) - DISTRHO_SAFE_ASSERT_RETURN(fSize.isValid(),) - - fRasterizer = nsvgCreateRasterizer(); + DISTRHO_SAFE_ASSERT_RETURN(rawData != nullptr, ) + DISTRHO_SAFE_ASSERT_RETURN(scaling > 0.0f, ) - const size_t bufferSize = width * height * 4; + NSVGrasterizer* rasterizer = nsvgCreateRasterizer(); // nsvgParse modifies the input data, so we must use a temporary buffer + const size_t bufferSize = dataSize * 4; + char tmpBuffer[bufferSize]; - strncpy(tmpBuffer, data, bufferSize); + strncpy(tmpBuffer, rawData, bufferSize); - const float dpi = 96 * scaling; - fImage = nsvgParse(tmpBuffer, "px", dpi); + const float dpi = 96; + + NSVGimage* image = nsvgParse(tmpBuffer, "px", dpi); - DISTRHO_SAFE_ASSERT_RETURN(fImage != nullptr,) + DISTRHO_SAFE_ASSERT_RETURN(image != nullptr, ) - const uint scaledWidth = width * scaling; - const uint scaledHeight = height * scaling; + const uint scaledWidth = image->width * scaling; + const uint scaledHeight = image->height * scaling; - fRGBAData = (unsigned char *)malloc(scaledWidth * scaledHeight * 4); + DISTRHO_SAFE_ASSERT_RETURN(scaledWidth > 0 && scaledHeight > 0, ) - nsvgRasterize(fRasterizer, fImage, 0, 0, 1, fRGBAData, scaledWidth, scaledHeight, scaledWidth * 4); + fRGBAData = (unsigned char*)malloc(scaledWidth * scaledHeight * 4); + + nsvgRasterize(rasterizer, image, 0, 0, scaling, fRGBAData, scaledWidth, scaledHeight, scaledWidth * 4); + + fSize.setSize(scaledWidth, scaledHeight); + + nsvgDelete(image); + nsvgDeleteRasterizer(rasterizer); } SVG::~SVG() { - nsvgDelete(fImage); - nsvgDeleteRasterizer(fRasterizer); - free(fRGBAData); } diff --git a/examples/MidiKeyboard/DistrhoPluginInfo.h b/examples/MidiKeyboard/DistrhoPluginInfo.h index dfdc7788..26545163 100644 --- a/examples/MidiKeyboard/DistrhoPluginInfo.h +++ b/examples/MidiKeyboard/DistrhoPluginInfo.h @@ -28,8 +28,7 @@ #define DISTRHO_PLUGIN_WANT_MIDI_INPUT 1 #define DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 1 -#define DISTRHO_UI_USE_NANOVG 1 -#define DISTRHO_UI_USER_RESIZABLE 1 +#define DISTRHO_UI_USER_RESIZABLE 1 #define DISTRHO_PLUGIN_LV2_CATEGORY "lv2:MIDIPlugin" diff --git a/examples/MidiKeyboard/KeyboardWidget.hpp b/examples/MidiKeyboard/KeyboardWidget.hpp index 135bcda8..6ef69dbc 100644 --- a/examples/MidiKeyboard/KeyboardWidget.hpp +++ b/examples/MidiKeyboard/KeyboardWidget.hpp @@ -1,28 +1,485 @@ #include "Widget.hpp" - +#include "Image.hpp" +#include "SVG.hpp" +#include "resources/MidiKeyboardResources.hpp" START_NAMESPACE_DISTRHO +class PianoKey +{ +public: + PianoKey() + : fPressed(false), + fImageNormal(), + fImageDown(), + fIndex(-1) + { + + } + + /** + Set the images that will be used when drawing the key. + */ + void setImages(const Image& imageNormal, const Image& imageDown) + { + fImageNormal = imageNormal; + fImageDown = imageDown; + + fBoundingBox.setSize(imageNormal.getWidth(), imageNormal.getHeight()); + } + + /** + Set the state of the key. + */ + void setPressed(bool pressed) noexcept + { + fPressed = pressed; + } + + /** + Indicate if the key is currently down. + */ + bool isPressed() noexcept + { + return fPressed; + } + + /** + Set the note index of the key. + */ + void setIndex(const int index) + { + fIndex = index; + } + + /** + Get the note index of the key. + */ + int getIndex() + { + return fIndex; + } + + /** + Determine if a point intersects with the key's bounding box. + */ + bool contains(Point point) noexcept + { + return fBoundingBox.contains(point); + } + + /** + Set the position of the key, relative to its parent KeyboardWidget. + */ + void setPosition(const int x, const int y) noexcept + { + fBoundingBox.setPos(x, y); + } + + /** + Get the width of the key. + */ + uint getWidth() noexcept + { + return fBoundingBox.getWidth(); + } + + /** + Draw the key at its bounding box's position. + */ + void draw() + { + Image* img; + + if (isPressed()) + { + img = &fImageDown; + } + else + { + img = &fImageNormal; + } + + img->drawAt(fBoundingBox.getPos()); + } + +private: + Rectangle fBoundingBox; + bool fPressed; + Image fImageNormal; + Image fImageDown; + int fIndex; +}; + class KeyboardWidget : public Widget { public: - KeyboardWidget(Window& parent) : Widget(parent) + class Callback + { + public: + virtual ~Callback() {} + virtual void keyboardKeyPressed(const uint keyIndex) = 0; + virtual void keyboardKeyReleased(const uint keyIndex) = 0; + }; + + KeyboardWidget(Window& parent) + : Widget(parent), + fCallback(nullptr), + fMouseDown(false), + fHeldKey(nullptr) + { + fSVGs[kWhiteKeyResourceIndex].loadFromMemory(MidiKeyboardResources::white_keyData, + MidiKeyboardResources::white_keyDataSize, + 1.0f); + + fSVGs[kWhiteKeyPressedResourceIndex].loadFromMemory(MidiKeyboardResources::white_key_pressedData, + MidiKeyboardResources::white_key_pressedDataSize, + 1.0f); + + fSVGs[kBlackKeyResourceIndex].loadFromMemory(MidiKeyboardResources::black_keyData, + MidiKeyboardResources::black_keyDataSize, + 1.0f); + + fSVGs[kBlackKeyPressedResourceIndex].loadFromMemory(MidiKeyboardResources::black_key_pressedData, + MidiKeyboardResources::black_key_pressedDataSize, + 1.0f); + + for (int i = 0; i < kResourcesCount; ++i) + { + fImages[i].loadFromSVG(fSVGs[i]); + } + + const int whiteKeysTotalSpacing = kWhiteKeySpacing * kWhiteKeysCount; + + const uint width = fImages[kWhiteKeyResourceIndex].getWidth() * kWhiteKeysCount + whiteKeysTotalSpacing; + const uint height = fImages[kWhiteKeyResourceIndex].getHeight(); + + setSize(width, height); + + setupKeyLookupTable(); + setKeyImages(); + positionKeys(); + } + + /** + Set the 'pressed' state of a key in the keyboard. + */ + void setKeyPressed(const uint keyIndex, const bool pressed, const bool sendCallback = false) { + DISTRHO_SAFE_ASSERT_RETURN(keyIndex < kKeyCount, ) + + PianoKey& key = *fKeysLookup[keyIndex]; + + if (key.isPressed() == pressed) + return; + key.setPressed(pressed); + + if (fCallback != nullptr && sendCallback) + { + if (pressed) + { + fCallback->keyboardKeyPressed(keyIndex); + } + else + { + fCallback->keyboardKeyReleased(keyIndex); + } + } + + repaint(); } + void setCallback(Callback* callback) + { + fCallback = callback; + } + + /** + Draw the piano keys. + */ void onDisplay() override { - const SVG &svg = SVG(MidiKeyboardResources::black_keyData, - MidiKeyboardResources::black_keyWidth, - MidiKeyboardResources::black_keyHeight, - 1.0f); + // Draw the white keys. + for (int i = 0; i < kWhiteKeysCount; ++i) + { + fWhiteKeys[i].draw(); + } + + // Draw the black keys, on top of the white keys. + for (int i = 0; i < kBlackKeysCount; ++i) + { + fBlackKeys[i].draw(); + } + } + + /** + Get the key that is under the specified point. + Return nullptr if the point is not hovering any key. + */ + PianoKey* tryGetHoveredKey(const Point& point) + { + // Since the black keys are on top of the white keys, we check for a mouse event on the black ones first + for (int i = 0; i < kBlackKeysCount; ++i) + { + if (fBlackKeys[i].contains(point)) + { + return &fBlackKeys[i]; + } + } + + // Check for mouse event on white keys + for (int i = 0; i < kWhiteKeysCount; ++i) + { + if (fWhiteKeys[i].contains(point)) + { + return &fWhiteKeys[i]; + } + } + + return nullptr; + } + + /** + Handle mouse events. + */ + bool onMouse(const MouseEvent& ev) override + { + // We only care about left mouse button events. + if (ev.button != 1) + { + return false; + } + + fMouseDown = ev.press; + + if (!fMouseDown) + { + if (fHeldKey != nullptr) + { + setKeyPressed(fHeldKey->getIndex(), false, true); + fHeldKey = nullptr; + + return true; + } + } + + if (!contains(ev.pos)) + { + return false; + } + + PianoKey* key = tryGetHoveredKey(ev.pos); + + if (key != nullptr) + { + setKeyPressed(key->getIndex(), ev.press, true); + fHeldKey = key; + + return true; + } + + return false; + } + + /** + Handle mouse motion. + */ + bool onMotion(const MotionEvent& ev) override + { + if (!fMouseDown) + { + return false; + } + + PianoKey* key = tryGetHoveredKey(ev.pos); + + if (key != fHeldKey) + { + if (fHeldKey != nullptr) + { + setKeyPressed(fHeldKey->getIndex(), false, true); + } + + if (key != nullptr) + { + setKeyPressed(key->getIndex(), true, true); + } + + fHeldKey = key; + + repaint(); + } + + return true; + } + +private: + void setupKeyLookupTable() + { + int whiteKeysCounter = 0; + int blackKeysCounter = 0; + + for (int i = 0; i < kKeyCount; ++i) + { + if (isBlackKey(i)) + { + fKeysLookup[i] = &fBlackKeys[blackKeysCounter++]; + } + else + { + fKeysLookup[i] = &fWhiteKeys[whiteKeysCounter++]; + } + } + } + + void setKeyImages() + { + for (int i = 0; i < kWhiteKeysCount; ++i) + { + fWhiteKeys[i].setImages(fImages[kWhiteKeyResourceIndex], fImages[kWhiteKeyPressedResourceIndex]); + } + + for (int i = 0; i < kBlackKeysCount; ++i) + { + fBlackKeys[i].setImages(fImages[kBlackKeyResourceIndex], fImages[kBlackKeyPressedResourceIndex]); + } + } + + /** + Put the keys at their proper position in the keyboard. + */ + void positionKeys() + { + const int whiteKeyWidth = fImages[kWhiteKeyResourceIndex].getWidth(); + const int blackKeyWidth = fImages[kBlackKeyResourceIndex].getWidth(); + + int whiteKeysCounter = 0; + int blackKeysCounter = 0; - Image img = Image(); - img.loadFromSVG(svg); + for (int i = 0; i < kKeyCount; ++i) + { + const int totalSpacing = kWhiteKeySpacing * whiteKeysCounter; + + PianoKey* key; + int xPos; + + if (isBlackKey(i)) + { + key = &fBlackKeys[blackKeysCounter]; + xPos = whiteKeysCounter * whiteKeyWidth + totalSpacing - blackKeyWidth / 2; + + blackKeysCounter++; + } + else + { + key = &fWhiteKeys[whiteKeysCounter]; + xPos = whiteKeysCounter * whiteKeyWidth + totalSpacing; + + whiteKeysCounter++; + } + + key->setPosition(xPos, 0); + key->setIndex(i); + } + } + + /** + * Determine if a note at a certain index is associated with a white key. + */ + bool isWhiteKey(const uint noteIndex) + { + return !isBlackKey(noteIndex); + } + + /** + * Determine if a note at a certain index is associated with a black key. + */ + bool isBlackKey(const uint noteIndex) + { + // Bring the index down to the first octave + const uint adjustedIndex = noteIndex % 12; - img.drawAt(0,0); + return adjustedIndex == 1 + || adjustedIndex == 3 + || adjustedIndex == 6 + || adjustedIndex == 8 + || adjustedIndex == 10; } + + /** + * Identifiers used for accessing the graphical resources of the widget. + */ + enum Resources + { + kWhiteKeyResourceIndex = 0, + kWhiteKeyPressedResourceIndex = 1, + kBlackKeyResourceIndex = 2, + kBlackKeyPressedResourceIndex = 3, + kResourcesCount + }; + + /** + The number of octaves displayed in the keyboard. + */ + static constexpr int kOctaves = 2; + + /** + The number of white keys displayed in the keyboard. + */ + static constexpr int kWhiteKeysCount = 7 * kOctaves + 1; + + /** + The spacing in pixels between the white keys. + */ + static constexpr int kWhiteKeySpacing = 1; + + /** + The number of black keys in the keyboard. + */ + static constexpr int kBlackKeysCount = 5 * kOctaves; + + /** + The number of keys in the keyboard. + */ + static constexpr int kKeyCount = kWhiteKeysCount + kBlackKeysCount; + + /** + The keyboard's white keys. + */ + PianoKey fWhiteKeys[kWhiteKeysCount]; + + /** + The keyboard's black keys. + */ + PianoKey fBlackKeys[kBlackKeysCount]; + + /** + Zero-indexed lookup table that maps notes to piano keys. + In this example, 0 is equal to C4. + */ + PianoKey* fKeysLookup[kKeyCount]; + + /** + Graphical resources. + */ + SVG fSVGs[kResourcesCount]; + Image fImages[kResourcesCount]; + + /** + The callback pointer, used for notifying the UI of piano key presses and releases. + */ + Callback* fCallback; + + /** + Whether or not the left mouse button is currently pressed. + */ + bool fMouseDown; + + /** + The piano key that is currently pressed with the mouse. + */ + PianoKey* fHeldKey; }; -END_NAMESPACE_DISTRHO \ No newline at end of file +END_NAMESPACE_DISTRHO diff --git a/examples/MidiKeyboard/Makefile b/examples/MidiKeyboard/Makefile index 4d03d417..cc2def44 100644 --- a/examples/MidiKeyboard/Makefile +++ b/examples/MidiKeyboard/Makefile @@ -33,11 +33,18 @@ BASE_FLAGS += -I../../dgl/src/nanovg # -------------------------------------------------------------- # Enable all possible plugin types -ifeq ($(LINUX),true) +ifeq ($(HAVE_JACK),true) +ifeq ($(HAVE_OPENGL),true) TARGETS += jack endif +endif +ifeq ($(HAVE_OPENGL),true) +TARGETS += lv2_sep +else TARGETS += lv2_dsp +endif + TARGETS += vst all: $(TARGETS) diff --git a/examples/MidiKeyboard/MidiKeyboardExampleUI.cpp b/examples/MidiKeyboard/MidiKeyboardExampleUI.cpp index c6cdf4cf..ba3e0334 100644 --- a/examples/MidiKeyboard/MidiKeyboardExampleUI.cpp +++ b/examples/MidiKeyboard/MidiKeyboardExampleUI.cpp @@ -20,21 +20,24 @@ START_NAMESPACE_DISTRHO -/** - We need the rectangle class from DGL. - */ -using DGL::Rectangle; // ----------------------------------------------------------------------------------------------------------- -class MidiKeyboardExampleUI : public UI +class MidiKeyboardExampleUI : public UI, + public KeyboardWidget::Callback { public: /* constructor */ MidiKeyboardExampleUI() - : UI(512, 512) + : UI(800, 132), + fKeyboardWidget(getParentWindow()) { - //getParentWindow().focus(); + const uint keyboardDeltaWidth = getWidth() - fKeyboardWidget.getWidth(); + + fKeyboardWidget.setAbsoluteX(keyboardDeltaWidth / 2); + fKeyboardWidget.setAbsoluteY(getHeight() - fKeyboardWidget.getHeight()); + fKeyboardWidget.setCallback(this); + // Add a min-size constraint to the window, to make sure that it can't become too small setGeometryConstraints(getWidth(), getHeight(), true, true); } @@ -46,8 +49,9 @@ class MidiKeyboardExampleUI : public UI /** A parameter has changed on the plugin side. This is called by the host to inform the UI about parameter changes. + This plugin does not have any parameters, so we can leave this blank. */ - void parameterChanged(uint32_t index, float value) override + void parameterChanged(uint32_t, float) override { } @@ -56,33 +60,41 @@ class MidiKeyboardExampleUI : public UI /** The OpenGL drawing function. + Here, we set a custom background color. */ - void onNanoDisplay() override + void onDisplay() override { - const uint width = getWidth(); - const uint height = getHeight(); + glClearColor(17.f / 255.f, + 17.f / 255.f, + 17.f / 255.f, + 17.f / 255.f); - //KeyboardRenderer().draw(getContext()); + glClear(GL_COLOR_BUFFER_BIT); } /** - Mouse press event. - */ - bool onMouse(const MouseEvent &ev) override + Called when a note is pressed on the piano. + */ + void keyboardKeyPressed(const uint keyIndex) override { - return false; + const uint C4 = 60; + sendNote(0, C4 + keyIndex, 127); } - bool onKeyboard(const KeyboardEvent &ev) override + /** + Called when a note is released on the piano. + */ + void keyboardKeyReleased(const uint keyIndex) override { - fprintf(stderr, "%u\n", ev.key); - repaint(); - return false; + const uint C4 = 60; + sendNote(0, C4 + keyIndex, 0); } // ------------------------------------------------------------------------------------------------------- private: + KeyboardWidget fKeyboardWidget; + /** Set our UI class as non-copyable and add a leak detector just in case. */ diff --git a/examples/MidiKeyboard/resources/MidiKeyboardResources.hpp b/examples/MidiKeyboard/resources/MidiKeyboardResources.hpp index d062474f..171d1ff1 100644 --- a/examples/MidiKeyboard/resources/MidiKeyboardResources.hpp +++ b/examples/MidiKeyboard/resources/MidiKeyboardResources.hpp @@ -7,23 +7,15 @@ namespace MidiKeyboardResources { extern const char* black_keyData; const unsigned int black_keyDataSize = 72329; - const unsigned int black_keyWidth = 32; - const unsigned int black_keyHeight = 72; extern const char* black_key_pressedData; const unsigned int black_key_pressedDataSize = 72336; - const unsigned int black_key_pressedWidth = 32; - const unsigned int black_key_pressedHeight = 72; extern const char* white_keyData; const unsigned int white_keyDataSize = 72329; - const unsigned int white_keyWidth = 46; - const unsigned int white_keyHeight = 112; extern const char* white_key_pressedData; const unsigned int white_key_pressedDataSize = 72338; - const unsigned int white_key_pressedWidth = 46; - const unsigned int white_key_pressedHeight = 112; } #endif // BINARY_MIDIKEYBOARDRESOURCES_HPP