├── BrowserComponent.cpp ├── BrowserComponent.h ├── BrowserIntegration.cpp ├── BrowserIntegration.h ├── BrowserIntegrationClient.cpp ├── BrowserIntegrationClient.h ├── BrowserIntegrationPluginClient.cpp ├── BrowserIntegrationPluginClient.h ├── BrowserValueTreeSynchroniser.cpp ├── BrowserValueTreeSynchroniser.h ├── LICENSE ├── README.md ├── pre_build.sh ├── tomduncalf_juce_web_ui.cpp ├── tomduncalf_juce_web_ui.h └── tomduncalf_juce_web_ui.mm /BrowserComponent.cpp: -------------------------------------------------------------------------------- 1 | namespace tomduncalf 2 | { 3 | namespace BrowserIntegration 4 | { 5 | BrowserComponent::BrowserComponent(): juce::WindowsWebView2WebBrowserComponent (false) 6 | { 7 | loadUI(); 8 | } 9 | 10 | BrowserComponent::BrowserComponent (juce::String initialUrl): juce::WindowsWebView2WebBrowserComponent (false) 11 | { 12 | goToURL (initialUrl); 13 | } 14 | 15 | void BrowserComponent::loadUI() 16 | { 17 | #if JUCE_MAC || JUCE_IOS 18 | NSString* devServerIpNsString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"DevServerIP"]; 19 | // TODO [NSBundle mainBundle] refers to the plugin host when running as AudioUnit 20 | // so can't access plist - hardcoding for now 21 | auto devServerIp = devServerIpNsString == nil ? "127.0.0.1" : [devServerIpNsString UTF8String]; 22 | #else 23 | auto devServerIp = "127.0.0.1"; 24 | #endif 25 | 26 | #if BROWSER_INTEGRATION_USE_DEV_SERVER_IN_DEBUG && JUCE_DEBUG 27 | auto url = juce::String ("http://") + devServerIp + ":3000"; 28 | #else 29 | auto url = "file://" + juce::File::getSpecialLocation (juce::File::SpecialLocationType::currentApplicationFile) 30 | #if JUCE_MAC 31 | .getChildFile ("Contents") 32 | .getChildFile ("Resources") 33 | #endif 34 | .getChildFile ("build") 35 | .getChildFile ("index.html") 36 | .getFullPathName(); 37 | #endif 38 | 39 | DBG ("Loading URL: " + url); 40 | goToURL (url); 41 | } 42 | 43 | void BrowserComponent::sendMessage (const juce::var message, bool suppressLog) 44 | { 45 | const auto jsonMessage = juce::JSON::toString (message, true); 46 | const auto url = "javascript:" + jsCallbackName + "(" + jsonMessage + ")"; 47 | 48 | if (! suppressLog) 49 | DBG ("sendMessage: " << jsonMessage); 50 | 51 | goToURL (url); 52 | } 53 | 54 | bool BrowserComponent::pageAboutToLoad (const juce::String& newURL) 55 | { 56 | if (newURL.startsWith (urlSchema)) 57 | { 58 | if (onMessageCallback) 59 | { 60 | auto messageString = juce::URL::removeEscapeChars (newURL.substring (urlSchema.length())); 61 | auto message = juce::JSON::parse (messageString); 62 | 63 | DBG ("message: " << messageString); 64 | 65 | onMessageCallback (message); 66 | } 67 | else 68 | { 69 | DBG ("No onMessageCallback defined in BrowserComponent"); 70 | jassertfalse; 71 | } 72 | 73 | return false; 74 | } 75 | else 76 | return true; 77 | } 78 | 79 | void BrowserComponent::setOnMessageCallback (std::function cb) 80 | { 81 | onMessageCallback = cb; 82 | } 83 | 84 | void BrowserComponent::scriptMessageReceived (const juce::var messageBody) 85 | { 86 | onMessageCallback (messageBody); 87 | } 88 | }// namespace BrowserIntegration 89 | }// namespace tomduncalf 90 | -------------------------------------------------------------------------------- /BrowserComponent.h: -------------------------------------------------------------------------------- 1 | /** 2 | A BrowserComponent wraps the default juce WebBrowserComponent, adding 3 | functionality to communicate with the browser instance using a custom 4 | URL scheme to pass data. 5 | */ 6 | #pragma once 7 | 8 | namespace tomduncalf 9 | { 10 | namespace BrowserIntegration 11 | { 12 | class BrowserComponent : public juce::WindowsWebView2WebBrowserComponent 13 | { 14 | public: 15 | /** Create a browser component with the default URL (in debug mode, loading 16 | from the React dev server, and in release mode, loading from the built 17 | application bundle in the Resources directory) 18 | */ 19 | BrowserComponent(); 20 | 21 | /** Create a browser component with a specific URL */ 22 | BrowserComponent (const juce::String initialUrl); 23 | 24 | void sendMessage (const juce::var message, bool suppressLog = false); 25 | bool pageAboutToLoad (const juce::String& newURL) override; 26 | 27 | void setOnMessageCallback (std::function cb); 28 | 29 | void loadUI(); 30 | 31 | protected: 32 | std::function onMessageCallback; 33 | 34 | const juce::String urlSchema = "juce://"; 35 | const juce::String jsCallbackName = "receiveMessageFromJuce"; 36 | 37 | void scriptMessageReceived (const juce::var messageBody) override; 38 | }; 39 | }// namespace BrowserIntegration 40 | }// namespace tomduncalf 41 | -------------------------------------------------------------------------------- /BrowserIntegration.cpp: -------------------------------------------------------------------------------- 1 | namespace tomduncalf 2 | { 3 | namespace BrowserIntegration 4 | { 5 | BrowserIntegration::BrowserIntegration (BrowserComponent& b): browser (b) 6 | { 7 | browser.setOnMessageCallback ([this] (juce::var message) { 8 | handleMessage (message); 9 | }); 10 | } 11 | 12 | void BrowserIntegration::registerBrowserCallback (juce::String name, BrowserCallback callback) 13 | { 14 | auto hasExistingCallbacks = callbacksByEventName.find (name) != callbacksByEventName.end(); 15 | if (hasExistingCallbacks) 16 | callbacksByEventName[name].push_back (callback); 17 | else 18 | callbacksByEventName[name] = { callback }; 19 | } 20 | 21 | void BrowserIntegration::sendEventToBrowser (juce::String eventType, juce::var data, bool suppressLog) 22 | { 23 | auto* obj = new juce::DynamicObject(); 24 | obj->setProperty ("eventType", eventType); 25 | obj->setProperty ("data", data); 26 | 27 | browser.sendMessage (juce::var (obj), suppressLog); 28 | } 29 | 30 | void BrowserIntegration::handleMessage (juce::var message) 31 | { 32 | if (! message.hasProperty ("eventType")) 33 | { 34 | jassertfalse; // malformed message 35 | return; 36 | } 37 | 38 | auto eventType = message.getProperty ("eventType", ""); 39 | auto callbacks = callbacksByEventName.find (eventType); 40 | 41 | if (callbacks == callbacksByEventName.end()) 42 | { 43 | DBG ("No callbacks defined for " << eventType.toString()); 44 | jassertfalse; 45 | return; 46 | } 47 | 48 | for (auto& callback: callbacks->second) 49 | callback (message.getProperty ("data", juce::var())); 50 | } 51 | }// namespace BrowserIntegration 52 | }// namespace tomduncalf 53 | -------------------------------------------------------------------------------- /BrowserIntegration.h: -------------------------------------------------------------------------------- 1 | /** 2 | A BrowserIntegration instance wraps a BrowserComponent, providing methods 3 | to send JSON messages (wrapped in a juce::var) and register callbacks for 4 | receiving events from Javascript. 5 | 6 | Usually you will want to construct a single BrowserIntegration instance and 7 | pass a reference down to any other classes that want to interact with it. 8 | */ 9 | #pragma once 10 | 11 | namespace tomduncalf 12 | { 13 | namespace BrowserIntegration 14 | { 15 | using BrowserCallback = std::function; 16 | 17 | class BrowserIntegration 18 | { 19 | public: 20 | BrowserIntegration (BrowserComponent& browser); 21 | 22 | void registerBrowserCallback (juce::String name, BrowserCallback callback); 23 | void sendEventToBrowser (juce::String eventType, juce::var data, bool suppressLog = false); 24 | 25 | protected: 26 | void handleMessage (juce::var message); 27 | 28 | std::unordered_map> callbacksByEventName; 29 | 30 | BrowserComponent& browser; 31 | }; 32 | }// namespace BrowserIntegration 33 | }// namespace tomduncalf 34 | -------------------------------------------------------------------------------- /BrowserIntegrationClient.cpp: -------------------------------------------------------------------------------- 1 | namespace tomduncalf 2 | { 3 | namespace BrowserIntegration 4 | { 5 | BrowserIntegrationClient::BrowserIntegrationClient (juce::String n, BrowserIntegration& b) 6 | : clientName (n), 7 | browserIntegration (b) {} 8 | 9 | void BrowserIntegrationClient::registerBrowserCallback (juce::String name, BrowserCallback callback) 10 | { 11 | browserIntegration.registerBrowserCallback (clientName + "::" + name, callback); 12 | } 13 | 14 | void BrowserIntegrationClient::sendEventToBrowser (juce::String eventType, juce::var data, bool suppressLog) 15 | { 16 | browserIntegration.sendEventToBrowser (clientName + "::" + eventType, data, suppressLog); 17 | } 18 | }// namespace BrowserIntegration 19 | }// namespace tomduncalf 20 | -------------------------------------------------------------------------------- /BrowserIntegrationClient.h: -------------------------------------------------------------------------------- 1 | /** 2 | BrowserIntegrationClient is intended as a base class for any class 3 | that wants to communicate with a browser (although it can be used 4 | as an instance variable if preferred). There should only be one 5 | BrowserIntegration instance which is passed around as a reference. 6 | 7 | Each client is given a name, which should be unique and is prepended 8 | to any event names in order to namespace them. 9 | */ 10 | #pragma once 11 | 12 | namespace tomduncalf 13 | { 14 | namespace BrowserIntegration 15 | { 16 | class BrowserIntegrationClient 17 | { 18 | public: 19 | BrowserIntegrationClient (juce::String clientName, BrowserIntegration& browserIntegration); 20 | 21 | void registerBrowserCallback (juce::String name, BrowserCallback callback); 22 | void sendEventToBrowser (juce::String eventType, juce::var data, bool suppressLog = false); 23 | 24 | protected: 25 | juce::String clientName; 26 | BrowserIntegration& browserIntegration; 27 | }; 28 | }// namespace BrowserIntegration 29 | }// namespace tomduncalf 30 | -------------------------------------------------------------------------------- /BrowserIntegrationPluginClient.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace tomduncalf 4 | { 5 | namespace BrowserIntegration 6 | { 7 | BrowserIntegrationPluginClient::BrowserIntegrationPluginClient (BrowserIntegration& b, 8 | juce::AudioProcessorValueTreeState& p, 9 | juce::String f, 10 | juce::String c) 11 | : BrowserIntegrationClient (c, b), 12 | parameterValueTree (p), 13 | valueTreeSynchroniser (p.state, "PARAMETERS", b), 14 | pluginEditorSourceFile (f) 15 | { 16 | } 17 | 18 | // This is a separate method rather than called from the constructor because it needs 19 | // to be called after all the members of the class implementing it have been setup 20 | void BrowserIntegrationPluginClient::setupBrowserPluginIntegration() 21 | { 22 | registerBrowserCallback ("init", [this] (juce::var) { 23 | sendParameterMetadata(); 24 | valueTreeSynchroniser.sendFullSyncCallback(); 25 | }); 26 | 27 | registerBrowserCallback ("setParameter", [this] (juce::var data) { 28 | parameterValueTree.getParameterAsValue (data["id"].toString()).setValue (data["value"]); 29 | }); 30 | } 31 | 32 | void BrowserIntegrationPluginClient::sendParameterMetadata() 33 | { 34 | juce::Array parameterInfos; 35 | 36 | for (auto parameterState: parameterValueTree.state) 37 | { 38 | auto id = parameterState.getProperty ("id").toString(); 39 | auto rangedParameter = parameterValueTree.getParameter (id); 40 | 41 | auto* parameterInfo = new juce::DynamicObject(); 42 | parameterInfo->setProperty ("id", id); 43 | 44 | if (auto floatParameter = dynamic_cast (rangedParameter)) 45 | { 46 | parameterInfo->setProperty ("type", "float"); 47 | parameterInfo->setProperty ("label", floatParameter->label); 48 | parameterInfo->setProperty ("min", floatParameter->range.start); 49 | parameterInfo->setProperty ("max", floatParameter->range.end); 50 | parameterInfo->setProperty ("step", floatParameter->range.interval); 51 | } 52 | else if (auto intParameter = dynamic_cast (rangedParameter)) 53 | { 54 | parameterInfo->setProperty ("type", "int"); 55 | parameterInfo->setProperty ("label", intParameter->label); 56 | parameterInfo->setProperty ("min", intParameter->getRange().getStart()); 57 | parameterInfo->setProperty ("max", intParameter->getRange().getEnd()); 58 | parameterInfo->setProperty ("step", 1); 59 | } 60 | else if (auto boolParameter = dynamic_cast (rangedParameter)) 61 | { 62 | parameterInfo->setProperty ("type", "bool"); 63 | parameterInfo->setProperty ("label", boolParameter->label); 64 | } 65 | else if (auto choiceParameter = dynamic_cast (rangedParameter)) 66 | { 67 | parameterInfo->setProperty ("type", "choice"); 68 | parameterInfo->setProperty ("choices", choiceParameter->choices); 69 | parameterInfo->setProperty ("label", choiceParameter->label); 70 | } 71 | 72 | parameterInfos.add (juce::var (parameterInfo)); 73 | } 74 | 75 | sendEventToBrowser ("parameterMetadata", parameterInfos); 76 | 77 | #if JUCE_DEBUG && BROWSER_INTEGRATION_WRITE_PARAMETER_CONFIG_IN_DEBUG && !(JUCE_IOS || JUCE_ANDROID) 78 | writeParameterConfigForTs (parameterInfos); 79 | #endif 80 | } 81 | 82 | void BrowserIntegrationPluginClient::writeParameterConfigForTs (juce::Array parameterInfos) 83 | { 84 | juce::StringArray parameterIds; 85 | 86 | juce::String output = R"(// This file is autogenerated by the JUCE BrowserIntegrationPluginClient class, 87 | // by inspecting all the parameters in the AudioProcessorValueTreeState. 88 | // 89 | // It is updated whenever you run the app in debug mode with the macro 90 | // BROWSER_INTEGRATION_WRITE_PARAMETER_CONFIG_IN_DEBUG=1. 91 | // 92 | // You shouldn't need to edit this manually - if you add a new parameter, 93 | // running the app in debug mode should be enough to update this. 94 | 95 | import { ParameterModel } from "../../juceIntegration/models/ParameterModel"; 96 | 97 | export type ParametersType = { 98 | )"; 99 | 100 | for (auto parameterInfo: parameterInfos) 101 | { 102 | parameterIds.add ("\"" + parameterInfo["id"].toString() + "\""); 103 | 104 | juce::String type = "number"; 105 | if (parameterInfo["type"] == "bool") 106 | type = "boolean"; 107 | else if (parameterInfo["type"] == "choice") 108 | { 109 | juce::StringArray choiceTypes; 110 | for (auto choice: *(parameterInfo["choices"].getArray())) 111 | choiceTypes.add ("\"" + choice.toString() + "\""); 112 | 113 | type = choiceTypes.joinIntoString (" | "); 114 | } 115 | 116 | output += " " + parameterInfo["id"].toString() + ": ParameterModel<" + type + ">;\n"; 117 | } 118 | 119 | output += R"(}; 120 | 121 | export const PARAMETER_IDS = [ 122 | )" + parameterIds.joinIntoString (",\n ") 123 | + ",\n];\n"; 124 | 125 | juce::File tsFile (pluginEditorSourceFile 126 | .getParentDirectory() 127 | .getParentDirectory() 128 | .getChildFile ("ui") 129 | .getChildFile ("src") 130 | .getChildFile ("config") 131 | .getChildFile ("autogenerated") 132 | .getChildFile ("parameters.ts")); 133 | 134 | jassert (tsFile.existsAsFile()); 135 | 136 | tsFile.replaceWithText (output); 137 | 138 | DBG ("Parmeter types writen to parameters.ts"); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /BrowserIntegrationPluginClient.h: -------------------------------------------------------------------------------- 1 | /** 2 | BrowserIntegrationPluginClient is intended as a base class for a JUCE 3 | PluginEditor which wants to communicate with a web UI inside a 4 | BrowserComponent. 5 | 6 | It takes a reference to the plugin's AudioProcessorValueTreeState, and 7 | takes care of synchronising state changes to JS and handling state changes 8 | from JS. 9 | */ 10 | #pragma once 11 | 12 | namespace tomduncalf 13 | { 14 | namespace BrowserIntegration 15 | { 16 | class BrowserIntegrationPluginClient : public BrowserIntegrationClient 17 | { 18 | public: 19 | BrowserIntegrationPluginClient (BrowserIntegration& browserIntegration, 20 | juce::AudioProcessorValueTreeState& parameterValueTree, 21 | juce::String pluginEditorFilePath, 22 | juce::String clientName = "Plugin"); 23 | 24 | void setupBrowserPluginIntegration(); 25 | 26 | protected: 27 | juce::AudioProcessorValueTreeState& parameterValueTree; 28 | BrowserValueTreeSynchroniser valueTreeSynchroniser; 29 | 30 | juce::File pluginEditorSourceFile; 31 | 32 | void sendParameterMetadata(); 33 | void writeParameterConfigForTs (juce::Array parameterInfos); 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BrowserValueTreeSynchroniser.cpp: -------------------------------------------------------------------------------- 1 | namespace tomduncalf 2 | { 3 | namespace BrowserIntegration 4 | { 5 | #define VALUE_TREE_SYNCHRONISER_BATCH_CHANGES 1 6 | 7 | BrowserValueTreeSynchroniser::BrowserValueTreeSynchroniser (juce::ValueTree& vt, juce::Identifier id, BrowserIntegration& b) 8 | : juce::ValueTreeSynchroniser (vt), 9 | treeId (id), 10 | browserIntegration (b) 11 | { 12 | startTimerHz (60); 13 | } 14 | 15 | void BrowserValueTreeSynchroniser::stateChanged (const void* encodedChange, size_t encodedChangeSize) 16 | { 17 | // TODO could filter out redundant multiple changes to a single parameter 18 | // in a single batch here or elsewhere 19 | auto change = juce::Base64::toBase64 (encodedChange, encodedChangeSize); 20 | queuedChanges.add (change); 21 | 22 | #if VALUE_TREE_SYNCHRONISER_BATCH_CHANGES 23 | // Do nothing, the timer will pick this up 24 | #else 25 | flushUpdates(); 26 | #endif 27 | } 28 | 29 | void BrowserValueTreeSynchroniser::timerCallback() 30 | { 31 | flushUpdates(); 32 | } 33 | 34 | void BrowserValueTreeSynchroniser::flushUpdates() 35 | { 36 | if (queuedChanges.size() > 0) 37 | { 38 | auto* dataObj = new juce::DynamicObject(); 39 | dataObj->setProperty ("treeId", treeId.toString()); 40 | dataObj->setProperty ("changes", queuedChanges); 41 | browserIntegration.sendEventToBrowser ("valueTreeStateChange", dataObj); 42 | 43 | queuedChanges.clear(); 44 | } 45 | } 46 | }// namespace BrowserIntegration 47 | }// namespace tomduncalf 48 | -------------------------------------------------------------------------------- /BrowserValueTreeSynchroniser.h: -------------------------------------------------------------------------------- 1 | /** 2 | BrowerValueTreeSynchroniser is a juce::ValueTreeSynchroniser which sends 3 | its sync messages to a BrowserIntegration, for them to then be decoded 4 | on the JS side so it can maintain a copy of the value tree. 5 | */ 6 | #pragma once 7 | 8 | namespace tomduncalf 9 | { 10 | namespace BrowserIntegration 11 | { 12 | class BrowserValueTreeSynchroniser : public juce::ValueTreeSynchroniser, private juce::Timer 13 | { 14 | public: 15 | BrowserValueTreeSynchroniser (juce::ValueTree& vt, juce::Identifier id, BrowserIntegration& b); 16 | 17 | void stateChanged (const void* encodedChange, size_t encodedChangeSize) override; 18 | void flushUpdates(); 19 | 20 | protected: 21 | juce::Identifier treeId; 22 | juce::StringArray queuedChanges; 23 | BrowserIntegration& browserIntegration; 24 | 25 | private: 26 | void timerCallback() override; 27 | }; 28 | }// namespace BrowserIntegration 29 | }// namespace tomduncalf 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tom Duncalf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tomduncalf_juce_web_ui 2 | 3 | tomduncalf_juce_web_ui is a JUCE module providing helper classes to make it easy to integrate a web-based UI (running in a JUCE WebBrowserComponent) with a JUCE application (e.g. an audio engine). 4 | 5 | **Please see https://github.com/tomduncalf/WebUISynth for an example integration with a simple synth engine.** 6 | 7 | Ultimately the integration will be cross-platform on both desktop and mobile, but right now it has only been tested on macOS and iOS. 8 | 9 | ## Status 10 | 11 | Experimental, Work in progress - I have a [basic example](https://github.com/tomduncalf/WebUISynth) working, but need to add more features, documentation and a tutorial. Should not be considered stable in terms of APIs. 12 | 13 | See [the project board](https://github.com/tomduncalf/tomduncalf_juce_web_ui/projects/2) for a list of tasks. 14 | 15 | ## Features 16 | 17 | - "Batteries included" defaults, using [MobX](https://mobx.js.org/) and [React](https://reactjs.org/) to make working with your audio engine as straightforward as possible – for example, Parameters are exposed in a reactive manner via React Context, making hooking up your parameters [easy](https://github.com/tomduncalf/WebUISynth/blob/main/ui/src/components/Parameters.tsx). 18 | 19 | - Great developer experience – integration and communication is made as straightforward as possible on both the JUCE and browser sides (see [below](#key-components)), and features web developers love such as hot reloading and developer tools are all available. As the UI is "just a web page", your favourite libraries can be reused for functionality and styling too. 20 | 21 | - Good performance – in my initial tests, a web based UI seemed to perform similarly to a native JUCE UI in terms of CPU usage when playing back a sequence in a host with automation and a scope visualiser open. This area needs more research however! 22 | 23 | - Cross-platform (planned, currently Mac/iOS only) – every platform supported by JUCE now provides a mature WebView component, so web UIs should perform well across all platforms, both standalone and as a hosted plugin. 24 | 25 | ## Key components: 26 | 27 | - A [browser component](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/main/BrowserComponent.h) which supports sending messages to and from a web browser, using JUCE's support for calling Javascript functions directly to call into the browser, and custom additions to JUCE to allow sending messages directly back to C++ (needs to be integrated into JUCE - for now, requires using my fork and is Mac/iOS only - but greatly improves performance e.g. with multi touch updates). 28 | 29 | - A [base class](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/main/BrowserIntegrationClient.h) for any classes which want to communicate with the browser, allowing sending messages and registering callbacks in a namespaced way. 30 | 31 | - Automatic one-way state synchronisation from JUCE ValueTrees to a JS representation using a [ValueTreeSynchroniser](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/main/BrowserValueTreeSynchroniser.h) and a [TypeScript implementation](https://github.com/tomduncalf/WebUISynth/tree/main/ui/src/juceIntegration/valueTree) of the ValueTreeSynchroniser protocol and [ValueTree class](https://github.com/tomduncalf/WebUISynth/blob/a49c101dc078e8d41d63fed16fb40570db49bdcd/ui/src/juceIntegration/valueTree/ValueTree.ts). 32 | 33 | The synchronisation is batched into one message per frame (at 60hz) to reduce redundant messages back and forth - however, there's room for more optimisation here! 34 | 35 | - A [base class](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/main/BrowserIntegrationPluginClient.h) for plugins which automatically handles synchonising the plugins AudioProcessorValueTreeState to the browser, and [corresponding models](https://github.com/tomduncalf/WebUISynth/blob/main/ui/src/juceIntegration/models/) on the JS side which provide a representation of the parameters, with getters and setters for easy modification. 36 | 37 | [Auto-generated TypeScript definitions](https://github.com/tomduncalf/WebUISynth/blob/main/ui/src/config/autogenerated/parameters.ts) for your parameters (generated when run in debug mode) reduces the amount of manual integration to a minimum, and the use of MobX means keeping your UI reactive is simply a case of adding [observer](https://github.com/tomduncalf/WebUISynth/blob/main/ui/src/components/ParameterSlider.tsx#L13) to React components and accessing `Parameters` [from React's Context API](https://github.com/tomduncalf/WebUISynth/blob/a49c101dc078e8d41d63fed16fb40570db49bdcd/ui/src/components/Parameters.tsx#L7). 38 | 39 | - Lightweight mechanism for communicating between [TypeScript](https://github.com/tomduncalf/WebUISynth/blob/main/ui/src/juceIntegration/juceCommunication.ts#L8) and [C++](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/main/BrowserIntegrationClient.h#L21) by sending messages and registering callbacks. 40 | 41 | For an example of C++ to browser communication, see the implementation of the Scope in [C++](https://github.com/tomduncalf/WebUISynth/blob/a49c101dc078e8d41d63fed16fb40570db49bdcd/Source/ScopeDataSender.h#L61) and [TypeScript](https://github.com/tomduncalf/WebUISynth/blob/a49c101dc078e8d41d63fed16fb40570db49bdcd/ui/src/components/Scope.tsx#L7). 42 | 43 | For an example of browser to C++ communication, see the implementation of setting parameters in [C++](https://github.com/tomduncalf/tomduncalf_juce_web_ui/blob/862a2706417f8acc568a0c62a2b028d8b8dfc699/BrowserIntegrationPluginClient.cpp#L27) and [TypeScript](https://github.com/tomduncalf/WebUISynth/blob/a49c101dc078e8d41d63fed16fb40570db49bdcd/ui/src/juceIntegration/messages/pluginMessages.ts#L4). 44 | 45 | ## Documentation 46 | 47 | Documentation is still TODO – once I am happy with the APIs and functionality, I will provide documentation and a tutorial but for now, there are some code comments in the source code and the example [WebUISynth](https://github.com/tomduncalf/WebUISynth) integration. 48 | 49 | ## Motivation 50 | 51 | Building user interfaces in C++ can be challenging, especially for developers coming from a web background. Advances in the web world over the last decade including declarative UI frameworks such as [React](https://reactjs.org/), hot reloading, and the ever expanding capabilities of both CSS and JavaScript have made developing complex UIs considerably easier – going back to imperative code updating a UI and a full compile cycle each time you make a change can feel like quite a step backwards. 52 | 53 | If you are happy to build for just one platform, native frameworks offer a better experience (on MacOS/iOS at least), but if you are building a cross-platform app, options are more limited. I have worked extensively with React Native in combination with JUCE (see my [talk from ADC'19](https://www.youtube.com/watch?v=bsy0-mHcS4Y) about building an app with this stack), but on desktop React Native is still quite an immature option. Flutter is an interesting option in many ways, but it requires learning a new framework and language. 54 | 55 | JavaScript (and in particular React) is widely known and fairly easy to learn. Browser engines are incredibly powerful and remarkably performant, so in theory there's no reason why they shouldn't be suitable for building UIs for audio apps and plugins – indeed, a developer from Output spoke about their experiences building their Arcade plugin using web UI at [ADC'20](https://www.youtube.com/watch?v=XvsCaQd2VFE]). 56 | 57 | The difficulty often comes with combining and interacting between two separate worlds – you have to worry about keeping state in sync, how to pass messages between JS and C++, etc. This module aims to solve those problems as elegantly as possible, so you can focus on writing great software. 58 | -------------------------------------------------------------------------------- /pre_build.sh: -------------------------------------------------------------------------------- 1 | # Only start the dev server in debug mode, and for the "Shared Code" target of the build 2 | # (Xcode will try to run the script twice, once for the Shared Code and once for the actual build) 3 | if [ "$CONFIGURATION" == "Debug" ] && [[ $TARGET_NAME == *" - Shared Code" ]]; then 4 | for i in "${TARGET_BUILD_DIR}/../../"*.plist; do /usr/libexec/PlistBuddy -c "Set :DevServerIP `ipconfig getifaddr en0`" "$i"; done 5 | 6 | if nc -w 5 -z localhost 3000 ; then 7 | if ! curl -s "http://localhost:3000/" ; then 8 | echo "Port already in use, server is either not running or not running correctly" 9 | exit 2 10 | fi 11 | else 12 | osascript < 17 | 18 | #include 19 | #include 20 | // TODO add #if for this one to allow non-plugin use case? 21 | #include 22 | 23 | #include "BrowserComponent.h" 24 | #include "BrowserIntegration.h" 25 | #include "BrowserIntegrationClient.h" 26 | #include "BrowserValueTreeSynchroniser.h" 27 | #include "BrowserIntegrationPluginClient.h" 28 | -------------------------------------------------------------------------------- /tomduncalf_juce_web_ui.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #include "tomduncalf_juce_web_ui.cpp" 5 | --------------------------------------------------------------------------------