New upstream version 1.6.4+ds0
authorIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Tue, 20 Sep 2022 09:49:46 +0000 (11:49 +0200)
committerIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Tue, 20 Sep 2022 09:49:46 +0000 (11:49 +0200)
40 files changed:
CMakeLists.txt
docs/Build/Meson_build.md
docs/changelog.yml
faust-src/meterdsp.dsp [new file with mode: 0644]
jacktrip.pro
meson.build
releases/edge/mac-manifests.json
releases/edge/win-manifests.json
releases/stable/mac-manifests.json
releases/stable/win-manifests.json
src/AudioInterface.cpp
src/JackAudioInterface.cpp
src/JackAudioInterface.h
src/JackTrip.cpp
src/JackTrip.h
src/Meter.cpp [new file with mode: 0644]
src/Meter.h [new file with mode: 0644]
src/ProcessPlugin.h
src/Regulator.cpp
src/RtAudioInterface.cpp
src/RtAudioInterface.h
src/gui/Browse.qml
src/gui/Connected.qml
src/gui/Meter.qml [new file with mode: 0644]
src/gui/SectionHeading.qml [new file with mode: 0644]
src/gui/Settings.qml
src/gui/Studio.qml
src/gui/qjacktrip.qrc
src/gui/share.svg [new file with mode: 0644]
src/gui/virtualstudio.cpp
src/gui/virtualstudio.h
src/gui/vs.qml
src/gui/vsDevice.cpp
src/gui/vsDevice.h
src/gui/vsQmlClipboard.h [new file with mode: 0644]
src/gui/vsServerInfo.cpp
src/gui/vsServerInfo.h
src/jacktrip_globals.h
src/main.cpp
src/meterdsp.h [new file with mode: 0644]

index 5cb1697ad97cfd1608c46957486fce21e037030a..81717bf45a1f0aed473dc69d506307e08f0aad0d 100644 (file)
@@ -109,6 +109,7 @@ set(qjacktrip_SRC
   src/Regulator.cpp
   src/Compressor.cpp
   src/Limiter.cpp
+  src/Meter.cpp
   src/Reverb.cpp
   src/AudioTester.cpp
   src/Patcher.cpp
index 5ea4037c12b35fe759b45e82e43239290ee9ab24..b416f7dff59fd4bffdf2f12d9ca2eff0b43e8176 100644 (file)
@@ -8,7 +8,7 @@ find its documentation at [mesonbuild.com](https://mesonbuild.com/).
 === "Fedora"
 
     ```bash
-    dnf install meson qt5-qtbase-devel rtaudio-devel "pkgconfig(jack)" help2man
+    dnf install meson qt5-qtbase-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel rtaudio-devel "pkgconfig(jack)" help2man python3-jinja2 python3-pyyaml
     ```
 
 === "Debian/Ubuntu"
index fd026c3863e7b896093136a237fded0236e40715..b424738235cadc1767e0a0ca128d4ed7edccb5be 100644 (file)
@@ -1,3 +1,20 @@
+- Version: "1.6.4"
+  Date: 2022-09-16
+  Description:
+  - (added) Volume meters when connected to a Studio in VS mode
+  - (added) Validation of Linux desktop file in build steps
+  - (added) a copy invite link button to VS mode Studios
+  - (added) an advanced setting for buffer strategy in VS mode
+  - (updated) PLC workers now zero out the last good packet from stalled clients
+  - (updated) PLC distinguish stuck clients
+  - (updated) Fedora meson dependencies
+  - (fixed) UI is usable when Studio list in empty in VS mode
+  - (fixed) Crashing when unplugging a device while connected and using JACK
+  - (fixed) Launching from URL skipped setup
+  - (fixed) UI hanging when connecting to Studios in VS mode
+  - (fixed) "Refresh list" button disabling ui interaction in VS mode
+  - (fixed) Network stats not displaying on first connect after login (VS mode)
+  - (fixed) VS mode won't join studios when on warning or device setup screens
 - Version: "1.6.3"
   Date: 2022-08-23
   Description:
@@ -8,7 +25,7 @@
   Date: 2022-08-05
   Description:
   - (updated) Static Qt version for Linux builds
-  - (upated) cleaner, easier to read VS settings
+  - (updated) cleaner, easier to read VS settings
   - (updated) icons for 'Manage' and 'Settings' in VS mode
   - (added) human-readable locations in VS mode
   - (added) warning that cmake is not officially supported
diff --git a/faust-src/meterdsp.dsp b/faust-src/meterdsp.dsp
new file mode 100644 (file)
index 0000000..95f37da
--- /dev/null
@@ -0,0 +1,19 @@
+
+declare name "meter";
+declare version "1.0";
+declare author "Dominick Hing";
+declare license "MIT Style STK-4.2";
+declare description "VU Meter Faust Plugin for JackTrip";
+
+// Originally modified from https://github.com/grame-cncm/faust/blob/master-dev/examples/analysis/meter.dsp
+
+import("stdfaust.lib");
+
+process = peakMeter
+with {
+
+    round(n, x) = x
+        <: (ma.copysign(_, 1) : _ * (10 ^ n) <: int(_) , ma.frac : _, (_ >= 0.5) :> + : _ / (10 ^ n) ), _
+        : ma.copysign(_, _);
+    peakMeter = _ : max(ba.db2linear(-80), _) : ba.linear2db(_) : round(2, _);
+};
index 15cda35dff56bcd693b126e35387fb7b545e663d..80fc9c19971d79964d351bdc9a4797db9390bd2f 100644 (file)
@@ -206,6 +206,7 @@ HEADERS += src/DataProtocol.h \
            src/Limiter.h \
            src/Regulator.h \
            src/Reverb.h \
+           src/Meter.h \
            src/AudioTester.h \
            src/jacktrip_globals.h \
            src/jacktrip_types.h \
@@ -223,6 +224,7 @@ HEADERS += src/DataProtocol.h \
            src/compressordsp.h \
            src/limiterdsp.h \
            src/freeverbdsp.h \
+           src/meterdsp.h \
            src/SslServer.h \
            src/Auth.h
 #(Removed JackTripThread.h JackTripWorkerMessages.h NetKS.h TestRingBuffer.h ThreadPoolTest.h)
@@ -247,6 +249,7 @@ HEADERS += src/DataProtocol.h \
                src/gui/vsPinger.h \
                src/gui/vsPing.h \
                src/gui/vsUrlHandler.h \
+               src/gui/vsQmlClipboard.h \
                src/JTApplication.h
   }
   !noupdater:!linux-g++:!linux-g++-64 {
@@ -267,6 +270,7 @@ SOURCES += src/DataProtocol.cpp \
            src/Limiter.cpp \
            src/Regulator.cpp \
            src/Reverb.cpp \
+           src/Meter.cpp \
            src/AudioTester.cpp \
            src/jacktrip_globals.cpp \
            src/JackTripWorker.cpp \
index af9e535dfe98a4fcc250a28aa65ddc48d04a519a..8bb8b9f3dd1d10d21459ac189ab295a82e0f1bfe 100644 (file)
@@ -20,6 +20,7 @@ incdirs = []
 
 src = [        'src/DataProtocol.cpp',
        'src/JackTrip.cpp',
+       'src/ProcessPlugin.cpp',
        'src/AudioTester.cpp',
        'src/jacktrip_globals.cpp',
        'src/JackTripWorker.cpp',
@@ -34,6 +35,7 @@ src = [       'src/DataProtocol.cpp',
        'src/AudioInterface.cpp',
        'src/Compressor.cpp',
        'src/Limiter.cpp',
+       'src/Meter.cpp',
        'src/Reverb.cpp',
        'src/main.cpp',
        'src/SslServer.cpp',
@@ -41,6 +43,8 @@ src = [       'src/DataProtocol.cpp',
 
 moc_h = ['src/DataProtocol.h',
        'src/JackTrip.h',
+       'src/ProcessPlugin.h',
+       'src/Meter.h',
        'src/JackTripWorker.h',
        'src/PacketHeader.h',
        'src/Settings.h',
@@ -122,6 +126,7 @@ else
                        'src/gui/vsPinger.h',
                        'src/gui/vsPing.h',
                        'src/gui/vsUrlHandler.h',
+                       'src/gui/vsQmlClipboard.h',
                        'src/JTApplication.h'
                ]
                qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system')
index 7b26f5f5c5e01912a1a3583e59a728da0bfafd66..2622246a515fe7f1b7f147af851af2d0579629dc 100644 (file)
@@ -1,6 +1,36 @@
 {
     "app_name": "JackTrip",
     "releases": [
+        {
+            "version": "1.6.4-rc.2",
+            "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc2",
+            "download": {
+                "date": "2022-09-08T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc2/JackTrip-v1.6.4-rc2-macOS-x64-installer.pkg",
+                "downloadSize": 11554117,
+                "sha256": "c849c22583883027f94141b3878cebc85295c0a617640449d4f66862982f75c0"
+            }
+        },
+        {
+            "version": "1.6.4-rc.1",
+            "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc1",
+            "download": {
+                "date": "2022-09-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc1/JackTrip-v1.6.4-rc1-macOS-x64-installer.pkg",
+                "downloadSize": 11548759,
+                "sha256": "6e8af76766b164e40e7a44842a14e640cb3c0fac7b9b7067f9c75048d3354496"
+            }
+        },
+        {
+            "version": "1.6.3",
+            "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+            "download": {
+                "date": "2022-08-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.3/JackTrip-v1.6.3-macOS-x64-installer.pkg",
+                "downloadSize": 11533023,
+                "sha256": "0b597c62544e9813949f859cc7b7c13bef893f6896f96b34a19889faf4855116"
+            }
+        },
         {
             "version": "1.6.2",
             "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
index 6304bbb022863b3db9d55511ad0269f4ae3e78f2..2331018e8247beb2a13018fbdecc78c8925f1ead 100644 (file)
@@ -1,6 +1,36 @@
 {
     "app_name": "JackTrip",
     "releases": [
+        {
+            "version": "1.6.4-rc.2",
+            "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc2",
+            "download": {
+                "date": "2022-09-08T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc2/JackTrip-v1.6.4-rc2-Windows-x64-installer.msi",
+                "downloadSize": 43663360,
+                "sha256": "fc782f0f9547ff096eb99891745e5d80056090b05a179f0209bd7785b1b808a6"
+            }
+        },
+        {
+            "version": "1.6.4-rc.1",
+            "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc1",
+            "download": {
+                "date": "2022-09-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc1/JackTrip-v1.6.4-rc1-Windows-x64-installer.msi",
+                "downloadSize": 43651072,
+                "sha256": "fbe0f883429119b4c878e0dff4bb2c79f2ac0d995b9c947f7eb8cc70fa59bc67"
+            }
+        },
+        {
+            "version": "1.6.3",
+            "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+            "download": {
+                "date": "2022-08-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.3/JackTrip-v1.6.3-Windows-x64-installer.msi",
+                "downloadSize": 43606016,
+                "sha256": "83a4def2a8c8fde24d147d39e70f60ee144e9425828f34eda2340c23ce72b1da"
+            }
+        },
         {
             "version": "1.6.2",
             "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
index c91d622bf43d07b827e3bb7867f55693e5712cbc..49cf17d0a028bde654c5b83ca85e660745e12b60 100644 (file)
@@ -1,6 +1,16 @@
 {
     "app_name": "JackTrip",
     "releases": [
+        {
+            "version": "1.6.3",
+            "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+            "download": {
+                "date": "2022-08-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.3/JackTrip-v1.6.3-macOS-x64-installer.pkg",
+                "downloadSize": 11533023,
+                "sha256": "0b597c62544e9813949f859cc7b7c13bef893f6896f96b34a19889faf4855116"
+            }
+        },
         {
             "version": "1.6.2",
             "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
index 48486bbe7e956b9d683ec7073cf136bda833330e..74cb51df0672375c976575fe9f5b3cdb70db2a8b 100644 (file)
@@ -1,6 +1,16 @@
 {
     "app_name": "JackTrip",
     "releases": [
+        {
+            "version": "1.6.3",
+            "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+            "download": {
+                "date": "2022-08-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.3/JackTrip-v1.6.3-Windows-x64-installer.msi",
+                "downloadSize": 43606016,
+                "sha256": "83a4def2a8c8fde24d147d39e70f60ee144e9425828f34eda2340c23ce72b1da"
+            }
+        },
         {
             "version": "1.6.2",
             "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
index a6dc55be7c558791381208a1c4333b047e206b0f..1244aaa779975dea59efc23e05b16e0339c2bcdd 100644 (file)
@@ -682,9 +682,13 @@ void AudioInterface::initPlugins()
         std::cout << "Initializing Faust plugins (have " << nPlugins
                   << ") at sampling rate " << mSampleRate << "\n";
         for (ProcessPlugin* plugin : qAsConst(mProcessPluginsFromNetwork)) {
+            plugin->setOutgoingToNetwork(false);
+            plugin->updateNumChannels(mNumInChans, mNumOutChans);
             plugin->init(mSampleRate);
         }
         for (ProcessPlugin* plugin : qAsConst(mProcessPluginsToNetwork)) {
+            plugin->setOutgoingToNetwork(true);
+            plugin->updateNumChannels(mNumInChans, mNumOutChans);
             plugin->init(mSampleRate);
         }
     }
index 56254351dba168797ccda78d59a0bec973accca5..516e9b9fdc1f3d061d536f3d1b20ce3e5682c9b1 100644 (file)
@@ -153,7 +153,7 @@ void JackAudioInterface::setupClient()
     }
 
     // Set function to call if Jack shuts down
-    jack_on_shutdown(mClient, this->jackShutdown, 0);
+    jack_on_info_shutdown(mClient, this->jackShutdown, 0);
 
     // Create input and output channels
     createChannels();
@@ -267,14 +267,11 @@ int JackAudioInterface::stopProcess() const
 }
 
 //*******************************************************************************
-void JackAudioInterface::jackShutdown(void*)
+void JackAudioInterface::jackShutdown(jack_status_t code, const char* reason, void* arg)
 {
-    // std::cout << "The Jack Server was shut down!" << std::endl;
+    std::cout << reason << std::endl;
     JackTrip::sJackStopped = true;
     std::cout << "The Jack Server was shut down!" << std::endl;
-    // throw std::runtime_error("The Jack Server was shut down!");
-    // std::cout << "Exiting program..." << std::endl;
-    // std::exit(1);
 }
 
 //*******************************************************************************
index a3f663319a1eb80129eeb0306645a2a83a101579..81ee103ff45b50088933b1b6ad474e7d149be152 100644 (file)
@@ -142,9 +142,9 @@ class JackAudioInterface : public AudioInterface
     /// \brief Creates input and output channels in the Jack client
     void createChannels();
     /** \brief JACK calls this shutdown_callback if the server ever shuts down or
-     * decides to disconnect the client.
+     * decides to disconnect the client and has a message to deliver.
      */
-    static void jackShutdown(void*);
+    static void jackShutdown(jack_status_t code, const char* reason, void* arg);
     /** \brief Set the process callback of the member function processCallback.
      * This process will be called by the JACK server whenever there is work to be done.
      */
index 5031bebd27b02577f08ca5b5991670c558d5a822..9d21357ddd922d288f8f89f81e9558a6db164d56 100644 (file)
@@ -588,9 +588,11 @@ void JackTrip::completeConnection()
     for (auto& i : mProcessPluginsFromNetwork) {
         mAudioInterface->appendProcessPluginFromNetwork(i);
     }
+
     for (auto& i : mProcessPluginsToNetwork) {
         mAudioInterface->appendProcessPluginToNetwork(i);
     }
+
     mAudioInterface->initPlugins();   // mSampleRate known now, which plugins require
     mAudioInterface->startProcess();  // Tell JACK server we are ready for audio flow now
 
index fa877d08ccf960a4a28c661d9bc3f462bc2b8118..99cb23abdca2ae73997bb8a17452458f70ab88e2 100644 (file)
@@ -624,8 +624,9 @@ class JackTrip : public QObject
     DataProtocol::packetHeaderTypeT mPacketHeaderType;  ///< Packet Header Type
     JackTrip::audiointerfaceModeT mAudiointerfaceMode;
 
-    int mNumAudioChansIn;    ///< Number of Audio Input Channels
-    int mNumAudioChansOut;   ///< Number of Audio Output Channels
+    int mNumAudioChansIn;   ///< Number of Audio Input Channels
+    int mNumAudioChansOut;  ///< Number of Audio Output Channels
+
 #ifdef WAIR                  // WAIR
     int mNumNetRevChans;     ///< Number of Network Audio Channels (net comb filters)
 #endif                       // endwhere
diff --git a/src/Meter.cpp b/src/Meter.cpp
new file mode 100644 (file)
index 0000000..a58f8b6
--- /dev/null
@@ -0,0 +1,151 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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 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.
+*/
+//*****************************************************************
+
+/**
+ * \file Meter.cpp
+ * \author Dominick Hing
+ * \date August 2022
+ * \license MIT
+ */
+
+#include "Meter.h"
+
+#include <QVector>
+
+#include "jacktrip_types.h"
+
+//*******************************************************************************
+void Meter::init(int samplingRate)
+{
+    ProcessPlugin::init(samplingRate);
+    if (samplingRate != fSamplingFreq) {
+        std::cerr << "Sampling rate not set by superclass!\n";
+        std::exit(1);
+    }
+
+    fs = float(fSamplingFreq);
+    for (int i = 0; i < mNumChannels; i++) {
+        meterP[i]->init(fs);
+    }
+
+    /* Set meter values to the default floor */
+    mValues.resize(mNumChannels);
+    QVector<float>::iterator it;
+    for (it = mValues.begin(); it != mValues.end(); ++it) {
+        *it = threshold;
+    }
+
+    /* Start timer */
+    int timeout_ms = 100;
+    connect(&mTimer, &QTimer::timeout, this, &Meter::onTick);
+    mTimer.setTimerType(Qt::PreciseTimer);
+    mTimer.setInterval(timeout_ms);
+    mTimer.setSingleShot(false);
+    mTimer.start();
+
+    inited = true;
+}
+
+//*******************************************************************************
+void Meter::compute(int nframes, float** inputs, float** /*_*/)
+{
+    // Note that the second parameter is unused. This is because all of the ProcessPlugins
+    // require the same function signature for the compute() function and is normally used
+    // for the faust plugin output. However, this plugin is not supposed to modify the
+    // signal itself like the other plugins (e.g. Limiter) do, so we don't want to write
+    // to this buffer. We just need to report the VU meter output
+
+    if (not inited) {
+        std::cerr << "*** Meter " << this << ": init never called! Doing it now.\n";
+        if (fSamplingFreq <= 0) {
+            fSamplingFreq = 48000;
+            std::cout << "Meter " << this
+                      << ": *** HAD TO GUESS the sampling rate (chose 48000 Hz) ***\n";
+        }
+        init(fSamplingFreq);
+    }
+
+    QVector<float> meterBuf(nframes);
+    float* meterBufPtr = meterBuf.data();
+    float** output     = &meterBufPtr;
+    for (int i = 0; i < mNumChannels; i++) {
+        /* Run the signal through Faust  */
+        meterP[i]->compute(nframes, &inputs[i], output);
+
+        /* Use the existing value of mValues[i] as
+           the threshold - this will be reset to the default floor of -80dB
+           on each timeout */
+        float max = mValues[i];
+        QVector<float>::iterator it;
+        for (it = meterBuf.begin(); it != meterBuf.end(); ++it) {
+            if (*it > max) {
+                max = *it;
+            }
+        }
+
+        /* Update mValues */
+        mValues[i] = max;
+    }
+
+    /* Set processed audio flag */
+    hasProcessedAudio = true;
+}
+
+//*******************************************************************************
+void Meter::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+
+    mValues.resize(mNumChannels);
+    QVector<float>::iterator it;
+    for (it = mValues.begin(); it != mValues.end(); ++it) {
+        *it = threshold;
+    }
+}
+
+//*******************************************************************************
+void Meter::onTick()
+{
+    if (hasProcessedAudio) {
+        /* Send the measurements to whatever other component requests it */
+        emit onComputedVolumeMeasurements(mValues);
+
+        /* Set meter values to the default floor */
+        QVector<float>::iterator it;
+        for (it = mValues.begin(); it != mValues.end(); ++it) {
+            *it = threshold;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Meter.h b/src/Meter.h
new file mode 100644 (file)
index 0000000..a475dc0
--- /dev/null
@@ -0,0 +1,103 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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 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.
+*/
+//*****************************************************************
+
+/**
+ * \file Meter.h
+ * \author Dominick Hing
+ * \date August 2022
+ * \license MIT
+ */
+
+#ifndef __METER_H__
+#define __METER_H__
+
+#include <QObject>
+#include <QTimer>
+#include <QVector>
+#include <iostream>
+#include <vector>
+
+#include "ProcessPlugin.h"
+#include "meterdsp.h"
+
+/** \brief The Meter class measures the live audio loudness level
+ */
+class Meter : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to measure
+    Meter(int numchans, bool verboseFlag = false) : mNumChannels(numchans)
+    {
+        setVerbose(verboseFlag);
+        for (int i = 0; i < mNumChannels; i++) {
+            meterP.push_back(new meterdsp);
+            // meterUIP.push_back(new APIUI);
+            // meterP[i]->buildUserInterface(meterUIP[i]);
+        }
+    }
+
+    /// \brief The class destructor
+    virtual ~Meter()
+    {
+        for (int i = 0; i < mNumChannels; i++) {
+            delete meterP[i];
+        }
+        meterP.clear();
+    }
+
+    void init(int samplingRate) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "VU Meter"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+   private:
+    float fs;
+    int mNumChannels;
+    float threshold = -80.0;
+    std::vector<meterdsp*> meterP;
+    bool hasProcessedAudio = false;
+
+    QTimer mTimer;
+    QVector<float> mValues;
+
+   private slots:
+    void onTick();
+
+   signals:
+    void onComputedVolumeMeasurements(QVector<float> values);
+};
+
+#endif
\ No newline at end of file
index 18e99b32a6ab306fe131bd62cec937607ae8d0ea..d0aeab391709874873f822f8f2b13857e1cc9daf 100644 (file)
@@ -38,6 +38,7 @@
 #ifndef __PROCESSPLUGIN_H__
 #define __PROCESSPLUGIN_H__
 
+#include <QObject>
 #include <QThread>
 
 /** \brief Interface for the process plugins to add to the JACK callback process in
  * methods except init, which is optional for processing that are sampling rate dependent
  * or that need specific initialization.
  */
-class ProcessPlugin
+class ProcessPlugin : public QObject
 {
+    Q_OBJECT;
+
    public:
     /// \brief The Class Constructor
     ProcessPlugin(){};
@@ -79,13 +82,28 @@ class ProcessPlugin
     virtual bool getInited() { return inited; }
     virtual void setVerbose(bool v) { verbose = v; }
 
+    virtual void setOutgoingToNetwork(bool toNetwork)
+    {
+        outgoingPluginToNetwork = toNetwork;
+    }
+
     /// \brief Compute process
     virtual void compute(int nframes, float** inputs, float** outputs) = 0;
 
+    /**
+     * @brief This function may optionally be used by plugins. This is useful
+     * if the number of audio channels in the parent audio interface has changed
+     * after the plugin instance was instantiated, to tell the plugin to modify its
+     * functionality.
+     */
+    virtual void updateNumChannels(int /*nChansIn*/, int /*nChansOut*/) { return; };
+
    protected:
-    int fSamplingFreq;  ///< Faust Data member, Sampling Rate
-    bool inited  = false;
-    bool verbose = false;
+    int fSamplingFreq;  //< Faust Data member, Sampling Rate
+    bool inited                  = false;
+    bool verbose                 = false;
+    bool outgoingPluginToNetwork = false;  //< Tells the plugin if it processes audio
+                                           // going into or out of the network
 };
 
 #endif
index 24a6c7dadddd8e276559aa57b81aaa74600db9f1..56a950a9863fc9977e1d48d81df762fd12c21db8 100644 (file)
@@ -382,6 +382,19 @@ void Regulator::pullPacket(int8_t* buf)
                 goto PACKETOK;
             }
         }
+        // make this a global value? -- same threshold as
+        // UdpDataProtocol::printUdpWaitedTooLong
+        double wait_time = 30;  // msec
+        if ((mLastSeqNumOut == mLastSeqNumIn)
+            && ((now - mIncomingTiming[mLastSeqNumOut]) > wait_time)) {
+            //                        std::cout << (mIncomingTiming[mLastSeqNumOut] - now)
+            //                        << "mLastSeqNumIn: " << mLastSeqNumIn <<
+            //                        "\tmLastSeqNumOut: " << mLastSeqNumOut << std::endl;
+            goto ZERO_OUTPUT;
+        }  // "good underrun", not a stuck client
+        //                    std::cout << "within window -- mLastSeqNumIn: " <<
+        //                    mLastSeqNumIn <<
+        //                    "\tmLastSeqNumOut: " << mLastSeqNumOut << std::endl;
         goto UNDERRUN;
     }
 
index 0b221c237311ec1e7f402ffa559322e62d4c15e7..3c8a62ece2ff851f4186eacac96e11970aa8a759 100644 (file)
@@ -159,7 +159,7 @@ void RtAudioInterface::setup()
         // be accessible
         mRtAudio->openStream(&out_params, &in_params, RTAUDIO_FLOAT32, sampleRate,
                              &bufferFrames, &RtAudioInterface::wrapperRtAudioCallback,
-                             this, &options);
+                             this, &options, &RtAudioInterface::RtAudioErrorCallback);
         setBufferSize(bufferFrames);
     } catch (RtAudioError& e) {
         std::cout << '\n' << e.getMessage() << '\n' << std::endl;
@@ -307,6 +307,16 @@ int RtAudioInterface::wrapperRtAudioCallback(void* outputBuffer, void* inputBuff
         outputBuffer, inputBuffer, nFrames, streamTime, status);
 }
 
+//*******************************************************************************
+void RtAudioInterface::RtAudioErrorCallback(RtAudioError::Type type,
+                                            const std::string& errorText)
+{
+    if ((type != RtAudioError::WARNING) && (type != RtAudioError::DEBUG_WARNING)) {
+        std::cout << '\n' << errorText << '\n' << std::endl;
+        throw std::runtime_error(errorText);
+    }
+}
+
 //*******************************************************************************
 int RtAudioInterface::startProcess() const
 {
index 51b6e9de2e0817b3ebd88e3193099c017903dc83..1397e688d5a1f017d1f84da715884952763d98e6 100644 (file)
@@ -84,6 +84,8 @@ class RtAudioInterface : public AudioInterface
     static int wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
                                       unsigned int nFrames, double streamTime,
                                       RtAudioStreamStatus status, void* userData);
+    static void RtAudioErrorCallback(RtAudioError::Type type,
+                                     const std::string& errorText);
     void printDeviceInfo(unsigned int deviceId);
 
     int mNumInChans;   ///< Number of Input Channels
index c4558ff954285c1a33acadb7a9fe346e0cadd76f..d36870c1157ee0a7594d6fbe748b328e31b55506 100644 (file)
@@ -16,6 +16,10 @@ Item {
     property int buttonHeight: 25
     property int buttonWidth: 103
     property int extraSettingsButtonWidth: 16
+    property int emptyListMessageWidth: 450
+    property int createMessageTopMargin: 16
+    property int createButtonTopMargin: 48
+    property int fontBig: 28
     property int fontMedium: 11
     
     property int scrollY: 0
@@ -35,7 +39,7 @@ Item {
         if (currentIndex == -1) {
             currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
         }
-        virtualstudio.refreshStudios(currentIndex)
+        virtualstudio.refreshStudios(currentIndex, true)
     }
     
     Rectangle {
@@ -51,182 +55,6 @@ Item {
         }
     }
     
-    Component {
-        id: sectionHeading
-        Rectangle {
-            color: "transparent"
-            height: 72 * virtualstudio.uiScale; x: 16 * virtualstudio.uiScale; width: ListView.view.width - (2 * x)
-            // required property string section: section (for 5.15)
-            Text {
-                id: sectionText
-                //anchors.bottom: parent.bottom
-                y: 12 * virtualstudio.uiScale
-                // text: parent.section (for 5.15)
-                text: section
-                font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
-                color: textColour
-            }
-            Button {
-                id: createButton
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: createButton.down ? "#E7E8E8" : "#F2F3F3"
-                    border.width: 1
-                    border.color: createButton.down ? "#B0B5B5" : "#EAEBEB"
-                    layer.enabled: createButton.hovered && !createButton.down
-                    layer.effect: DropShadow {
-                        horizontalOffset: 1 * virtualstudio.uiScale
-                        verticalOffset: 1 * virtualstudio.uiScale
-                        radius: 8.0 * virtualstudio.uiScale
-                        samples: 17
-                        color: "#80A1A1A1"
-                    }
-                }
-                onClicked: { virtualstudio.createStudio(); }
-                anchors.right: filterButton.left
-                anchors.rightMargin: 16
-                anchors.verticalCenter: sectionText.verticalCenter
-                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "Create a Studio"
-                    font.family: "Poppins"
-                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                    font.weight: Font.Bold
-                    color: "#DB0A0A"
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
-                visible: section == virtualstudio.logoSection ? true : false
-            }
-            Button {
-                id: filterButton
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: filterButton.down ? "#E7E8E8" : "#F2F3F3"
-                    border.width: 1
-                    border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB"
-                    layer.enabled: filterButton.hovered && !filterButton.down
-                    layer.effect: DropShadow {
-                        horizontalOffset: 1 * virtualstudio.uiScale
-                        verticalOffset: 1 * virtualstudio.uiScale
-                        radius: 8.0 * virtualstudio.uiScale
-                        samples: 17
-                        color: "#80A1A1A1"
-                    }
-                }
-                onClicked: { filterMenu.open(); }
-                anchors.right: parent.right
-                anchors.verticalCenter: sectionText.verticalCenter
-                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "Filter Studios"
-                    font.family: "Poppins"
-                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
-                visible: section == virtualstudio.logoSection ? true : false
-
-                Popup {
-                    id: filterMenu
-                    y: Math.round(parent.height + 8)
-                    rightMargin: 16 * virtualstudio.uiScale
-                    width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
-                    modal: false
-                    focus: false
-                    closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: "#F6F8F8"
-                        border.width: 1
-                        border.color: "#34979797"
-                        layer.enabled: true
-                        layer.effect: DropShadow {
-                            horizontalOffset: 1 * virtualstudio.uiScale
-                            verticalOffset: 1 * virtualstudio.uiScale
-                            radius: 8.0 * virtualstudio.uiScale
-                            samples: 17
-                            color: "#80A1A1A1"
-                        }
-                    }
-                    contentItem: Column {
-                        anchors.fill: parent
-                        CheckBox {
-                            id: inactiveCheckbox
-                            text: qsTr("Show my inactive Studios")
-                            checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked
-                            onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked;
-                                refreshing = true;
-                                refresh();
-                            }
-                            indicator: Rectangle {
-                                implicitWidth: 16 * virtualstudio.uiScale
-                                implicitHeight: 16 * virtualstudio.uiScale
-                                x: inactiveCheckbox.leftPadding
-                                y: parent.height / 2 - height / 2
-                                radius: 3 * virtualstudio.uiScale
-                                border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
-
-                                Rectangle {
-                                    width: 10 * virtualstudio.uiScale
-                                    height: 10 * virtualstudio.uiScale
-                                    x: 3 * virtualstudio.uiScale
-                                    y: 3 * virtualstudio.uiScale
-                                    radius: 2 * virtualstudio.uiScale
-                                    color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
-                                    visible: inactiveCheckbox.checked
-                                }
-                            }
-                            contentItem: Text {
-                                text: inactiveCheckbox.text
-                                font.family: "Poppins"
-                                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
-                                anchors.horizontalCenter: parent.horizontalCenter
-                                anchors.verticalCenter: parent.verticalCenter
-                                leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing
-                            }
-                        }
-                        CheckBox {
-                            id: selfHostedCheckbox
-                            text: qsTr("Show self-hosted Studios")
-                            checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked
-                            onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked;
-                                refreshing = true;
-                                refresh();
-                            }
-                            indicator: Rectangle {
-                                implicitWidth: 16 * virtualstudio.uiScale
-                                implicitHeight: 16 * virtualstudio.uiScale
-                                x: selfHostedCheckbox.leftPadding
-                                y: parent.height / 2 - height / 2
-                                radius: 3 * virtualstudio.uiScale
-                                border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
-
-                                Rectangle {
-                                    width: 10 * virtualstudio.uiScale
-                                    height: 10 * virtualstudio.uiScale
-                                    x: 3 * virtualstudio.uiScale
-                                    y: 3 * virtualstudio.uiScale
-                                    radius: 2 * virtualstudio.uiScale
-                                    color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
-                                    visible: selfHostedCheckbox.checked
-                                }
-                            }
-                            contentItem: Text {
-                                text: selfHostedCheckbox.text
-                                font.family: "Poppins"
-                                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
-                                anchors.horizontalCenter: parent.horizontalCenter
-                                anchors.verticalCenter: parent.verticalCenter
-                                leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-    
     Component {
         id: footer
         Rectangle {
@@ -256,9 +84,103 @@ Item {
             manageable: isManageable
             available: canConnect
             connected: false
+            studioId: id ? id : ""
+            inviteKeyString: inviteKey ? inviteKey : ""
         }
         
-        section {property: "type"; criteria: ViewSection.FullString; delegate: sectionHeading }
+        section {property: "type"; criteria: ViewSection.FullString; delegate: SectionHeading {} }
+
+        // Show sectionHeading if there are no Studios in list
+        SectionHeading {
+            id: emptyListSectionHeading
+            listIsEmpty: true
+            visible: parent.count == 0 && !virtualstudio.showCreateStudio
+        }
+
+        Text {
+            id: emptyListMessage
+            visible: parent.count == 0 && !virtualstudio.showCreateStudio
+            text: "No studios found that match your filter criteria"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: emptyListMessageWidth
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: emptyListSectionHeading.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Rectangle {
+            id: newUserEmptyList
+            anchors.fill: parent
+            color: "transparent"
+            visible: parent.count == 0 && virtualstudio.showCreateStudio
+
+            Rectangle {
+                color: "transparent"
+                width: emptyListMessageWidth
+                height: createButton.height + createStudioMessage.height + welcomeMessage.height + createButtonTopMargin + createMessageTopMargin
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+
+                Text {
+                    id: welcomeMessage
+                    text: "Welcome"
+                    font { family: "Poppins"; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
+                    color: textColour
+                    width: emptyListMessageWidth
+                    wrapMode: Text.Wrap
+                    horizontalAlignment: Text.AlignHCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.top: parent.top
+                }
+
+                Text {
+                    id: createStudioMessage
+                    text: "JackTrip works by connecting your computer's audio to a Virtual Studio. Create your first Studio to get started!"
+                    font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                    width: emptyListMessageWidth
+                    wrapMode: Text.Wrap
+                    horizontalAlignment: Text.AlignHCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.top: welcomeMessage.bottom
+                    anchors.topMargin: createMessageTopMargin
+                }
+
+                Button {
+                    id: createButton
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: createButton.down ? "#E7E8E8" : "#F2F3F3"
+                        border.width: 1
+                        border.color: createButton.down ? "#B0B5B5" : "#EAEBEB"
+                        layer.enabled: createButton.hovered && !createButton.down
+                        layer.effect: DropShadow {
+                            horizontalOffset: 1 * virtualstudio.uiScale
+                            verticalOffset: 1 * virtualstudio.uiScale
+                            radius: 8.0 * virtualstudio.uiScale
+                            samples: 17
+                            color: "#80A1A1A1"
+                        }
+                    }
+                    onClicked: { virtualstudio.createStudio(); }
+                    anchors.top: createStudioMessage.bottom
+                    anchors.topMargin: createButtonTopMargin
+                    anchors.horizontalCenter: createStudioMessage.horizontalCenter
+                    width: 210 * virtualstudio.uiScale; height: 45 * virtualstudio.uiScale
+                    Text {
+                        text: "Create a Studio"
+                        font.family: "Poppins"
+                        font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+                        font.weight: Font.Bold
+                        color: "#DB0A0A"
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+                }
+            }
+        }
 
         // Disable momentum scroll
         MouseArea {
index 29ffbe51a79e37394ceccdec5a94437dcf63367d..2f9f1b42be8c714f50757c56e2fae3bfa6f46838 100644 (file)
@@ -8,16 +8,54 @@ Item {
     
     property bool connecting: false
     
-    property int leftMargin: 16
+    property int leftHeaderMargin: 16
     property int fontBig: 28
-    property int fontMedium: 18
-    property int fontSmall: 11
+    property int fontMedium: 12
+    property int fontSmall: 10
+    property int fontTiny: 8
 
-    property int smallTextPadding: 8
+    property int bodyMargin: 60
     
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
     property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
 
+    property string meterGreen: "#61C554"
+    property string meterYellow: "#F5BF4F"
+    property string meterRed: "#F21B1B"
+
+    function getNetworkStatsText (networkStats) {
+        let minRtt = networkStats.minRtt;
+        let maxRtt = networkStats.maxRtt;
+        let avgRtt = networkStats.avgRtt;
+
+        let texts = ["Measuring stats ...", ""];
+
+        if (!minRtt || !maxRtt) {
+            return texts;
+        }
+
+        texts[0] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms round-trip time";
+
+        let quality = "poor";
+        if (avgRtt <= 25) {
+
+            if (maxRtt <= 30) {
+                quality = "excellent";
+            } else {
+                quality = "good";
+            }
+
+        } else if (avgRtt <= 30) {
+            quality = "good";
+        } else if (avgRtt <= 35) {
+            quality = "fair";
+        }
+
+        texts[1] = "Your connection quality is <b>" + quality + "</b>."
+        return texts;
+    }
+
     Image {
         x: parent.width - (49 * virtualstudio.uiScale); y: 16 * virtualstudio.uiScale
         width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
@@ -27,13 +65,13 @@ Item {
     Text {
         id: heading
         text: virtualstudio.connectionState
-        x: leftMargin * virtualstudio.uiScale; y: 34 * virtualstudio.uiScale
+        x: leftHeaderMargin * virtualstudio.uiScale; y: 34 * virtualstudio.uiScale
         font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
         color: textColour
     }
     
     Studio {
-        x: parent.leftMargin * virtualstudio.uiScale; y: 96 * virtualstudio.uiScale
+        x: leftHeaderMargin * virtualstudio.uiScale; y: 96 * virtualstudio.uiScale
         width: parent.width - (2 * x)
         connected: true
         serverLocation: virtualstudio.currentStudio >= 0 && virtualstudio.regions[serverModel[virtualstudio.currentStudio].location] ? "in " + virtualstudio.regions[serverModel[virtualstudio.currentStudio].location].label : ""
@@ -42,133 +80,173 @@ Item {
         publicStudio: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isPublic : false
         manageable: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable : false
         available: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].canConnect : false
-    }
-    
-    Image {
-        id: mic
-        source: "mic.svg"
-        x: 80 * virtualstudio.uiScale; y: 250 * virtualstudio.uiScale
-        width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+        studioId: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].id : ""
+        inviteKeyString: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].inviteKey : ""
     }
 
-    Colorize {
-        anchors.fill: mic
-        source: mic
-        hue: 0
-        saturation: 0
-        lightness: imageLightnessValue
-    }
-    
-    Image {
-        id: headphones
-        source: "headphones.svg"
-        anchors.horizontalCenter: mic.horizontalCenter
-        y: 329 * virtualstudio.uiScale
-        width: 24 * virtualstudio.uiScale; height: 26 * virtualstudio.uiScale
-    }
+    Item {
+        id: inputDevice
+        x: bodyMargin * virtualstudio.uiScale; y: 250 * virtualstudio.uiScale
+        width: Math.min(parent.width / 2, 320 * virtualstudio.uiScale) - x
+        height: 100 * virtualstudio.uiScale
+        clip: true
+
+        Image {
+            id: mic
+            source: "mic.svg"
+            x: 0; y: 0
+            width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+        }
 
-    Image {
-        id: network
-        source: "network.svg"
-        anchors.horizontalCenter: mic.horizontalCenter
-        y: 408 * virtualstudio.uiScale
-        width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
-    }
+        Colorize {
+            anchors.fill: mic
+            source: mic
+            hue: 0
+            saturation: 0
+            lightness: imageLightnessValue
+        }
 
-    Colorize {
-        anchors.fill: headphones
-        source: headphones
-        hue: 0
-        saturation: 0
-        lightness: imageLightnessValue
-    }
+        Text {
+            id: inputDeviceHeader
+            x: 64 * virtualstudio.uiScale
+            width: parent.width - 64 * virtualstudio.uiScale
+            text: "<b>Input Device</b>"
+            font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors.verticalCenter: mic.verticalCenter
+            color: textColour
+            elide: Text.ElideRight
+        }
 
-    Colorize {
-        anchors.fill: network
-        source: network
-        hue: 0
-        saturation: 0
-        lightness: imageLightnessValue
-    }
-    
-    Text {
-        id: inputDeviceHeader
-        x: 120 * virtualstudio.uiScale
-        text: virtualstudio.audioBackend == "JACK" ? 
-            virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice]
-        font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-        anchors.verticalCenter: mic.verticalCenter
-        color: textColour
-    }
-    
-    Text {
-        id: outputDeviceHeader
-        x: 120 * virtualstudio.uiScale
-        text: virtualstudio.audioBackend == "JACK" ? 
-            virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice]
-        font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-        anchors.verticalCenter: headphones.verticalCenter
-        color: textColour
+        Text {
+            id: inputDeviceName
+            width: parent.width - 100 * virtualstudio.uiScale
+            anchors.top: inputDeviceHeader.bottom
+            anchors.left: inputDeviceHeader.left
+            text: virtualstudio.audioBackend == "JACK" ?
+                virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice]
+            font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            elide: Text.ElideRight
+        }
     }
 
-    Text {
-        id: networkStatsHeader
-        x: 120 * virtualstudio.uiScale
-        text: "Network"
-        font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-        anchors.verticalCenter: network.verticalCenter
-        color: textColour
-    }
+    Item {
+        id: outputDevice
+        x: bodyMargin * virtualstudio.uiScale; y: 330 * virtualstudio.uiScale
+        width: Math.min(parent.width / 2, 320 * virtualstudio.uiScale) - x
+        height: 100 * virtualstudio.uiScale
+        clip: true
+
+        Image {
+            id: headphones
+            source: "headphones.svg"
+            x: 0; y: 0
+            width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+        }
 
-    function getNetworkStatsText (networkStats) {
-        let minRtt = networkStats.minRtt;
-        let maxRtt = networkStats.maxRtt;
-        let avgRtt = networkStats.avgRtt;
+        Colorize {
+            anchors.fill: headphones
+            source: headphones
+            hue: 0
+            saturation: 0
+            lightness: imageLightnessValue
+        }
 
-        let texts = ["Measuring stats ...", "", ""];
+        Text {
+            id: outputDeviceHeader
+            x: 64 * virtualstudio.uiScale
+            width: parent.width - 64 * virtualstudio.uiScale
+            text: "<b>Output Device</b>"
+            font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors.verticalCenter: headphones.verticalCenter
+            color: textColour
+            elide: Text.ElideRight
+        }
 
-        if (!minRtt || !maxRtt) {
-            return texts;
+        Text {
+            id: outputDeviceName
+            width: parent.width - 100 * virtualstudio.uiScale
+            anchors.top: outputDeviceHeader.bottom
+            anchors.left: outputDeviceHeader.left
+            text: virtualstudio.audioBackend == "JACK" ?
+                virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice]
+            font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            elide: Text.ElideRight
         }
+    }
 
-        texts[0] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms round-trip time";
+    Meter {
+        id: inputDeviceMeters
+        x: inputDevice.x + inputDevice.width; y: 250 * virtualstudio.uiScale
+        width: parent.width - inputDevice.width - 2 * bodyMargin * virtualstudio.uiScale
+        height: 100 * virtualstudio.uiScale
+        model: inputMeterModel
+        clipped: inputClipped
+    }
 
-        let quality = "poor";
-        if (avgRtt <= 25) {
+    Meter {
+        id: outputDeviceMeters
+        x: outputDevice.x + outputDevice.width; y: 330 * virtualstudio.uiScale
+        width: parent.width - inputDevice.width - 2 * bodyMargin * virtualstudio.uiScale
+        height: 100 * virtualstudio.uiScale
+        model: outputMeterModel
+        clipped: outputClipped
+    }
 
-            if (maxRtt <= 30) {
-                quality = "excellent";
-            } else {
-                quality = "good";
-            }
+    Item {
+        id: networkStatsHeader
+        x: bodyMargin * virtualstudio.uiScale; y: 410 * virtualstudio.uiScale
+        width: Math.min(parent.width / 2, 320 * virtualstudio.uiScale) - x
+        height: 128 * virtualstudio.uiScale
+
+        Image {
+            id: network
+            source: "network.svg"
+            x: 0; y: 0
+            width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+        }
 
-        } else if (avgRtt <= 30) {
-            quality = "good";
-        } else if (avgRtt <= 35) {
-            quality = "fair";
+        Colorize {
+            anchors.fill: network
+            source: network
+            hue: 0
+            saturation: 0
+            lightness: imageLightnessValue
         }
 
-        texts[1] = "Your connection quality is <b>" + quality + "</b>."
-        return texts;
+        Text {
+            id: networkStatsHeaderText
+            text: "<b>Network</b>"
+            font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            x: 64 * virtualstudio.uiScale
+            anchors.verticalCenter: network.verticalCenter
+            color: textColour
+        }
     }
 
-    Text {
-        id: netstat0
-        text: getNetworkStatsText(virtualstudio.networkStats)[0]
-        font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-        topPadding: smallTextPadding
-        anchors.left: inputDeviceHeader.left
-        anchors.top: networkStatsHeader.bottom
-        color: textColour
-    }
+    Item {
+        id: networkStatsText
+        x: networkStatsHeader.x + networkStatsHeader.width; y: 410 * virtualstudio.uiScale
+        width: parent.width - networkStatsHeader.width - 2 * bodyMargin * virtualstudio.uiScale
+        height: 128 * virtualstudio.uiScale
+
+        Text {
+            id: netstat0
+            x: 0; y: 0
+            text: getNetworkStatsText(virtualstudio.networkStats)[0]
+            font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
 
-    Text {
-        id: netstat1
-        text: getNetworkStatsText(virtualstudio.networkStats)[1]
-        font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-        topPadding: smallTextPadding
-        anchors.left: inputDeviceHeader.left
-        anchors.top: netstat0.bottom
-        color: textColour
+        Text {
+            id: netstat1
+            x: 0
+            text: getNetworkStatsText(virtualstudio.networkStats)[1]
+            font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+            topPadding: 8 * virtualstudio.uiScale
+            anchors.top: netstat0.bottom
+            color: textColour
+        }
     }
 }
diff --git a/src/gui/Meter.qml b/src/gui/Meter.qml
new file mode 100644 (file)
index 0000000..f3a3a3d
--- /dev/null
@@ -0,0 +1,214 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    required property var model
+    property int bins: 15
+
+    property int innerMargin: 2 * virtualstudio.uiScale
+    property int clipWidth: 10 * virtualstudio.uiScale
+    required property bool clipped
+
+    property string meterColor: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+
+    property string meterGreen: "#61C554"
+    property string meterYellow: "#F5BF4F"
+    property string meterRed: "#F21B1B"
+
+    function getBoxColor (idx, level) {
+
+        // Case where the meter should be filled
+        if (level > (idx / bins)) {
+            let fillColor = meterGreen;
+            if (idx > 8 && idx <= 11) {
+                fillColor = meterYellow;
+            } else if (idx > 11) {
+                fillColor = meterRed;
+            }
+            return fillColor;
+
+        // Case where the meter should not be filled
+        } else {
+            return meterColor
+        }
+    }
+
+    ListView {
+        id: meters
+        x: 0; y: 0
+        width: parent.width - clipWidth
+        height: parent.height
+        model: parent.model
+
+        delegate: Item {
+            x: 0;
+            width: parent.width
+            height: 14 * virtualstudio.uiScale
+            required property var modelData
+
+            property int boxHeight: 10 * virtualstudio.uiScale
+            property int boxWidth: (width / bins) - innerMargin
+            property int boxRadius: 4 * virtualstudio.uiScale
+
+            Rectangle {
+                id: box0
+                x: 0;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(0, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box1
+                x: boxWidth + innerMargin;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(1, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box2
+                x: (boxWidth) * 2 + innerMargin * 2;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(2, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box3
+                x: (boxWidth) * 3 + innerMargin * 3;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(3, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box4
+                x: (boxWidth) * 4 + innerMargin * 4;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(4, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box5
+                x: (boxWidth) * 5 + innerMargin * 5;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(5, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box6
+                x: (boxWidth) * 6 + innerMargin * 6;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(6, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box7
+                x: (boxWidth) * 7 + innerMargin * 7;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(7, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box8
+                x: (boxWidth) * 8 + innerMargin * 8;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(8, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box9
+                x: (boxWidth) * 9 + innerMargin * 9;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(9, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box10
+                x: (boxWidth) * 10 + innerMargin * 10;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(10, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box11
+                x: (boxWidth) * 11 + innerMargin * 11;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(11, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box12
+                x: (boxWidth) * 12 + innerMargin * 12;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(12, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box13
+                x: (boxWidth) * 13 + innerMargin * 13;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(13, parent.modelData.level)
+                radius: boxRadius
+            }
+
+            Rectangle {
+                id: box14
+                x: (boxWidth) * 14 + innerMargin * 14;
+                y: 0;
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(14, parent.modelData.level)
+                radius: boxRadius
+            }
+        }
+    }
+
+    Rectangle {
+        id: clipIndicator
+        x: meters.x + meters.width; y: 0
+        width: Math.min(clipWidth, ((parent.width - clipWidth) / bins) - innerMargin)
+        height: 24 * virtualstudio.uiScale
+        radius: 4 * virtualstudio.uiScale
+        color: clipped ? meterRed : meterColor
+    }
+}
\ No newline at end of file
diff --git a/src/gui/SectionHeading.qml b/src/gui/SectionHeading.qml
new file mode 100644 (file)
index 0000000..a996d0e
--- /dev/null
@@ -0,0 +1,183 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Rectangle {
+  property bool listIsEmpty: false
+  // required property string section: section (for 5.15)
+  color: "transparent"
+  height: 72 * virtualstudio.uiScale
+  x: 16 * virtualstudio.uiScale
+  y: listIsEmpty ? 16 * virtualstudio.uiScale : 0
+  width: listIsEmpty ? parent.width - (2 * x) : ListView.view.width - (2 * x)
+  Text {
+      id: sectionText
+      //anchors.bottom: parent.bottom
+      y: 12 * virtualstudio.uiScale
+      // text: parent.section (for 5.15)
+      width: parent.width - 332 * virtualstudio.uiScale
+      fontSizeMode: Text.HorizontalFit
+      text: listIsEmpty ? "No Studios" : section
+      font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
+      color: textColour
+      verticalAlignment: Text.AlignBottom
+  }
+  Button {
+      id: createButton
+      background: Rectangle {
+          radius: 6 * virtualstudio.uiScale
+          color: createButton.down ? "#E7E8E8" : "#F2F3F3"
+          border.width: 1
+          border.color: createButton.down ? "#B0B5B5" : "#EAEBEB"
+          layer.enabled: createButton.hovered && !createButton.down
+          layer.effect: DropShadow {
+              horizontalOffset: 1 * virtualstudio.uiScale
+              verticalOffset: 1 * virtualstudio.uiScale
+              radius: 8.0 * virtualstudio.uiScale
+              samples: 17
+              color: "#80A1A1A1"
+          }
+      }
+      onClicked: { virtualstudio.createStudio(); }
+      anchors.right: filterButton.left
+      anchors.rightMargin: 16
+      anchors.verticalCenter: sectionText.verticalCenter
+      width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+      Text {
+          text: "Create a Studio"
+          font.family: "Poppins"
+          font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+          font.weight: Font.Bold
+          color: "#DB0A0A"
+          anchors.horizontalCenter: parent.horizontalCenter
+          anchors.verticalCenter: parent.verticalCenter
+      }
+      visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false)
+  }
+  Button {
+      id: filterButton
+      background: Rectangle {
+          radius: 6 * virtualstudio.uiScale
+          color: filterButton.down ? "#E7E8E8" : "#F2F3F3"
+          border.width: 1
+          border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB"
+          layer.enabled: filterButton.hovered && !filterButton.down
+          layer.effect: DropShadow {
+              horizontalOffset: 1 * virtualstudio.uiScale
+              verticalOffset: 1 * virtualstudio.uiScale
+              radius: 8.0 * virtualstudio.uiScale
+              samples: 17
+              color: "#80A1A1A1"
+          }
+      }
+      onClicked: { filterMenu.open(); }
+      anchors.right: parent.right
+      anchors.verticalCenter: sectionText.verticalCenter
+      width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+      Text {
+          text: "Filter Studios"
+          font.family: "Poppins"
+          font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+          anchors.horizontalCenter: parent.horizontalCenter
+          anchors.verticalCenter: parent.verticalCenter
+      }
+      visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false)
+
+      Popup {
+          id: filterMenu
+          y: Math.round(parent.height + 8)
+          rightMargin: 16 * virtualstudio.uiScale
+          width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+          modal: false
+          focus: false
+          closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+          background: Rectangle {
+              radius: 6 * virtualstudio.uiScale
+              color: "#F6F8F8"
+              border.width: 1
+              border.color: "#34979797"
+              layer.enabled: true
+              layer.effect: DropShadow {
+                  horizontalOffset: 1 * virtualstudio.uiScale
+                  verticalOffset: 1 * virtualstudio.uiScale
+                  radius: 8.0 * virtualstudio.uiScale
+                  samples: 17
+                  color: "#80A1A1A1"
+              }
+          }
+          contentItem: Column {
+              anchors.fill: parent
+              CheckBox {
+                  id: inactiveCheckbox
+                  text: qsTr("Show my inactive Studios")
+                  checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked
+                  onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked;
+                      refreshing = true;
+                      refresh();
+                  }
+                  indicator: Rectangle {
+                      implicitWidth: 16 * virtualstudio.uiScale
+                      implicitHeight: 16 * virtualstudio.uiScale
+                      x: inactiveCheckbox.leftPadding
+                      y: parent.height / 2 - height / 2
+                      radius: 3 * virtualstudio.uiScale
+                      border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+
+                      Rectangle {
+                          width: 10 * virtualstudio.uiScale
+                          height: 10 * virtualstudio.uiScale
+                          x: 3 * virtualstudio.uiScale
+                          y: 3 * virtualstudio.uiScale
+                          radius: 2 * virtualstudio.uiScale
+                          color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+                          visible: inactiveCheckbox.checked
+                      }
+                  }
+                  contentItem: Text {
+                      text: inactiveCheckbox.text
+                      font.family: "Poppins"
+                      font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                      anchors.horizontalCenter: parent.horizontalCenter
+                      anchors.verticalCenter: parent.verticalCenter
+                      leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing
+                  }
+              }
+              CheckBox {
+                  id: selfHostedCheckbox
+                  text: qsTr("Show self-hosted Studios")
+                  checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked
+                  onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked;
+                      refreshing = true;
+                      refresh();
+                  }
+                  indicator: Rectangle {
+                      implicitWidth: 16 * virtualstudio.uiScale
+                      implicitHeight: 16 * virtualstudio.uiScale
+                      x: selfHostedCheckbox.leftPadding
+                      y: parent.height / 2 - height / 2
+                      radius: 3 * virtualstudio.uiScale
+                      border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+
+                      Rectangle {
+                          width: 10 * virtualstudio.uiScale
+                          height: 10 * virtualstudio.uiScale
+                          x: 3 * virtualstudio.uiScale
+                          y: 3 * virtualstudio.uiScale
+                          radius: 2 * virtualstudio.uiScale
+                          color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+                          visible: selfHostedCheckbox.checked
+                      }
+                  }
+                  contentItem: Text {
+                      text: selfHostedCheckbox.text
+                      font.family: "Poppins"
+                      font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                      anchors.horizontalCenter: parent.horizontalCenter
+                      anchors.verticalCenter: parent.verticalCenter
+                      leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing
+                  }
+              }
+          }
+      }
+  }
+}
\ No newline at end of file
index 02a6f3d05625b80ab0cd9d54d980ae345df2760a..45d75b508c365b53211c450f96c6f277b6812a83 100644 (file)
@@ -105,6 +105,22 @@ Item {
                     color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour)
                 }
             }
+            Button {
+                id: advancedBtn
+                text: "Advanced"
+                width: parent.width
+                contentItem: Label {
+                    text: advancedBtn.text
+                    font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                    color: textColour
+                }
+                background: Rectangle {
+                    width: parent.width
+                    color: advancedBtn.down ? buttonPressedColour : (advancedBtn.hovered || settingsGroupView == "Advanced" ? buttonHoverColour : backgroundColour)
+                }
+            }
             Button {
                 id: profileBtn
                 text: "Profile"
@@ -294,18 +310,18 @@ Item {
         }
         
         Button {
-            id: modeButton
+            id: darkButton
             background: Rectangle {
                 radius: 6 * virtualstudio.uiScale
-                color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
+                color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
                 border.width: 1
-                border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
+                border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
             }
-            onClicked: { window.state = "login"; virtualstudio.toStandard(); }
-            x: parent.width - (232 * virtualstudio.uiScale); y: scaleSlider.y + (56 * virtualstudio.uiScale)
+            onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
+            x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (56 * virtualstudio.uiScale)
             width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
             Text {
-                text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode"
+                text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"
                 font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
                 anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
                 color: textColour
@@ -313,26 +329,36 @@ Item {
         }
 
         Text {
-            anchors.verticalCenter: modeButton.verticalCenter
+            anchors.verticalCenter: darkButton.verticalCenter
             x: leftMargin * virtualstudio.uiScale
-            text: "Display Mode"
+            text: "Color Theme"
             font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
             color: textColour
         }
-        
+    }
+
+    Rectangle {
+        id: advancedSettingsView
+        width: 0.8 * parent.width
+        height: parent.height - header.height
+        x: 0.2 * window.width
+        y: header.height
+        color: backgroundColour
+        visible: settingsGroupView == "Advanced"
+
         Button {
-            id: darkButton
+            id: modeButton
             background: Rectangle {
                 radius: 6 * virtualstudio.uiScale
-                color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
+                color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
                 border.width: 1
-                border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
+                border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
             }
-            onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
-            x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (56 * virtualstudio.uiScale)
+            onClicked: { window.state = "login"; virtualstudio.toStandard(); }
+            x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale
             width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
             Text {
-                text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"
+                text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode"
                 font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
                 anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
                 color: textColour
@@ -340,16 +366,16 @@ Item {
         }
 
         Text {
-            anchors.verticalCenter: darkButton.verticalCenter
+            anchors.verticalCenter: modeButton.verticalCenter
             x: leftMargin * virtualstudio.uiScale
-            text: "Color Theme"
+            text: "Display Mode"
             font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
             color: textColour
         }
 
         ComboBox {
             id: updateChannelCombo
-            x: 234 * virtualstudio.uiScale; y: darkButton.y + (56 * virtualstudio.uiScale)
+            x: 234 * virtualstudio.uiScale; y: modeButton.y + (48 * virtualstudio.uiScale)
             width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
             model: updateChannelComboModel
             currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1
@@ -366,6 +392,26 @@ Item {
             color: textColour
             visible: !virtualstudio.noUpdater
         }
+
+        ComboBox {
+            id: bufferStrategyCombo
+            x: updateChannelCombo.x; y: updateChannelCombo.y + (48 * virtualstudio.uiScale)
+            width: updateChannelCombo.width; height: updateChannelCombo.height
+            model: bufferStrategyComboModel
+            currentIndex: virtualstudio.bufferStrategy
+            onActivated: { virtualstudio.bufferStrategy = currentIndex }
+            font.family: "Poppins"
+            visible: virtualstudio.audioBackend != "JACK"
+        }
+
+        Text {
+            anchors.verticalCenter: bufferStrategyCombo.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Buffer Strategy"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            visible: virtualstudio.audioBackend != "JACK"
+            color: textColour
+        }
     }
 
     Rectangle {
index b087cc58e0ba34def8a5cffe22912ceee8d74f3f..9f07b58a1554ddac535e01fbb937572d3b121456 100644 (file)
@@ -1,6 +1,7 @@
 import QtQuick 2.12
 import QtQuick.Controls 2.12
 import QtGraphicalEffects 1.12
+import VS 1.0
 
 Rectangle {
     width: 664; height: 83 * virtualstudio.uiScale
@@ -21,13 +22,18 @@ Rectangle {
     property string serverLocation: "Germany - Berlin"
     property string flagImage: "flags/DE.svg"
     property string studioName: "Test Studio"
+    property string studioId: ""
+    property string inviteKeyString: ""
     property bool publicStudio: false
     property bool manageable: false
     property bool available: true
     property bool connected: false
+    property bool inviteCopied: false
     
     property int leftMargin: 81
     property int topMargin: 13
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
     
     property real fontBig: 18
     property real fontMedium: 11
@@ -35,7 +41,9 @@ Rectangle {
     
     property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6"
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+    property string toolTipBackgroundColour: inviteCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3")
+    property string toolTipTextColour: inviteCopied ? "#FAFBFB" : textColour
     property string joinColour: virtualstudio.darkMode ? (connected ? "#FCB6B6" : "#E2EBE0") : (connected ? "#FCB6B6" : "#C4F4BE")
     property string joinHoverColour: virtualstudio.darkMode ? (connected ? "#D49696" : "#BAC7B8") : (connected ? "#E3A4A4" : "#B0DCAB")
     property string joinPressedColour: virtualstudio.darkMode ? (connected ? "#F2AEAE" : "#D8E2D6") : (connected ? "#EFADAD" : "#BAE8B5")
@@ -45,6 +53,10 @@ Rectangle {
     property string managePressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB"
     property string manageStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494"
 
+    Clipboard {
+        id: clipboard
+    }
+
     Rectangle {
         id: shadow
         anchors.fill: parent
@@ -110,10 +122,12 @@ Rectangle {
     
     Text {
         x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale;
-        width: manageable ? parent.width - (233 * virtualstudio.uiScale) : parent.width - (156 * virtualstudio.uiScale)
+        width: manageable ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale)
         text: studioName
+        fontSizeMode: Text.HorizontalFit
         font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
         elide: Text.ElideRight
+        verticalAlignment: Text.AlignVCenter
         color: textColour
     }
     
@@ -141,7 +155,7 @@ Rectangle {
     
     Button {
         id: joinButton
-        x: manageable ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale)
+        x: manageable ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale)
         y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
         background: Rectangle {
             radius: width / 2
@@ -173,6 +187,79 @@ Rectangle {
         visible: connected || canConnect || canStart
         color: textColour
     }
+
+    Button {
+        id: inviteButton
+        x: manageable ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale)
+        y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: inviteButton.down ? managePressedColour : (inviteButton.hovered ? manageHoverColour : manageColour)
+            border.width:  inviteButton.down ? 1 : 0
+            border.color: manageStroke
+        }
+        Timer {
+            id: copiedResetTimer
+            interval: 2000; running: false; repeat: false
+            onTriggered: inviteCopied = false;
+        }
+        onClicked: { 
+            inviteCopied = true;
+            if (!inviteKeyString) {
+                clipboard.setText(qsTr("https://app.jacktrip.org/studios/" + studioId + "?invited=true"));
+            } else {
+                clipboard.setText(qsTr("https://app.jacktrip.org/studios/" + studioId + "?invited=" + inviteKeyString));
+            }
+            copiedResetTimer.restart()
+        }
+        visible: true
+        Image {
+            width: 20 * virtualstudio.uiScale; height: width
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "share.svg"
+        }
+        ToolTip {
+            parent: inviteButton
+            visible: inviteButton.hovered || inviteCopied
+            bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
+            rightPadding: rightToolTipMargin * virtualstudio.uiScale
+            delay: 100
+            contentItem: Rectangle {
+                color: toolTipBackgroundColour
+                radius: 3
+                anchors.fill: parent
+                anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
+                anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
+                layer.enabled: true
+                layer.effect: DropShadow {
+                    horizontalOffset: 1 * virtualstudio.uiScale
+                    verticalOffset: 1 * virtualstudio.uiScale
+                    radius: 10.0 * virtualstudio.uiScale
+                    samples: 21
+                    color: shadowColour
+                }
+
+                Text {
+                    anchors.centerIn: parent
+                    font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
+                    text: inviteCopied ?  qsTr("📋 Copied invitation link to Clipboard") : qsTr("Copy invite link for Studio")
+                    color: toolTipTextColour
+                }
+            }
+            background: Rectangle {
+                color: "transparent"
+            }
+        }
+    }
+    
+    Text {
+        anchors.horizontalCenter: inviteButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: "Invite"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: true
+        color: textColour
+    }
     
     Button {
         id: manageButton
index 6a5678c7d143cbe03cf09522c603d6a1072ccd4a..286174459cf29548df179e72768e70b489e5b9f3 100644 (file)
     <file>Studio.qml</file>
     <file>Browse.qml</file>
     <file>Settings.qml</file>
+    <file>Meter.qml</file>
     <file>Connected.qml</file>
     <file>Failed.qml</file>
     <file>Setup.qml</file>
+    <file>SectionHeading.qml</file>
     <file>logo.svg</file>
     <file>wedge.svg</file>
     <file>wedge_inactive.svg</file>
@@ -22,6 +24,7 @@
     <file>join.svg</file>
     <file>leave.svg</file>
     <file>manage.svg</file>
+    <file>share.svg</file>
     <file>cog.svg</file>
     <file>mic.svg</file>
     <file>ethernet.png</file>
diff --git a/src/gui/share.svg b/src/gui/share.svg
new file mode 100644 (file)
index 0000000..98a1ea2
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.8377 23.1914C17.9686 23.1914 17.2242 22.8816 16.6047 22.2621C15.9852 21.6426 15.6755 20.8983 15.6755 20.0291C15.6755 19.8997 15.6893 19.7471 15.7171 19.5714C15.7448 19.3957 15.7864 19.2339 15.8419 19.086L7.43689 14.2039C7.1595 14.5183 6.81738 14.7725 6.41054 14.9667C6.0037 15.1609 5.58761 15.258 5.16227 15.258C4.29311 15.258 3.54877 14.9482 2.92927 14.3287C2.30976 13.7092 2 12.9649 2 12.0957C2 11.208 2.30976 10.4591 2.92927 9.84882C3.54877 9.23856 4.29311 8.93343 5.16227 8.93343C5.58761 8.93343 5.99445 9.01664 6.3828 9.18308C6.77115 9.34951 7.12252 9.58992 7.43689 9.9043L15.8419 5.07767C15.7864 4.94822 15.7448 4.80028 15.7171 4.63384C15.6893 4.46741 15.6755 4.31022 15.6755 4.16227C15.6755 3.27462 15.9852 2.52566 16.6047 1.9154C17.2242 1.30513 17.9686 1 18.8377 1C19.7254 1 20.4743 1.30513 21.0846 1.9154C21.6949 2.52566 22 3.27462 22 4.16227C22 5.03144 21.6949 5.77577 21.0846 6.39529C20.4743 7.01479 19.7254 7.32455 18.8377 7.32455C18.4124 7.32455 18.0009 7.2552 17.6033 7.11651C17.2057 6.97781 16.8682 6.75127 16.5908 6.43689L8.18585 11.0971C8.22284 11.245 8.2552 11.4161 8.28294 11.6103C8.31068 11.8044 8.32455 11.9663 8.32455 12.0957C8.32455 12.2252 8.31068 12.3638 8.28294 12.5118C8.2552 12.6597 8.22284 12.8077 8.18585 12.9556L16.5908 17.7268C16.8682 17.4679 17.1919 17.2598 17.5617 17.1026C17.9316 16.9454 18.3569 16.8669 18.8377 16.8669C19.7254 16.8669 20.4743 17.172 21.0846 17.7822C21.6949 18.3925 22 19.1415 22 20.0291C22 20.8983 21.6949 21.6426 21.0846 22.2621C20.4743 22.8816 19.7254 23.1914 18.8377 23.1914Z" fill="#000000"/>
+</svg>
index edc79d5f4dfbabcf82827ae16f00edd7b10b661a..b2ccf5208b17d881465092a24ce194291f94f73d 100644 (file)
@@ -88,13 +88,30 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
     // Set our font scaling to convert points to pixels
     m_fontScale = 4.0 / 3.0;
 
-#ifdef RT_AUDIO
+    // Initialize timers needed for clip indicators
+    m_inputClipTimer.setTimerType(Qt::PreciseTimer);
+    m_inputClipTimer.setSingleShot(true);
+    m_inputClipTimer.setInterval(3000);
+    m_outputClipTimer.setTimerType(Qt::PreciseTimer);
+    m_outputClipTimer.setSingleShot(true);
+    m_outputClipTimer.setInterval(3000);
+
+    m_inputClipTimer.callOnTimeout([&]() {
+        m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputClipped"),
+                                                           QVariant::fromValue(false));
+    });
+
+    m_outputClipTimer.callOnTimeout([&]() {
+        m_view.engine()->rootContext()->setContextProperty(
+            QStringLiteral("outputClipped"), QVariant::fromValue(false));
+    });
+
     settings.beginGroup(QStringLiteral("Audio"));
-    m_useRtAudio   = settings.value(QStringLiteral("Backend"), 0).toInt() == 1;
-    m_inputDevice  = settings.value(QStringLiteral("InputDevice"), "").toString();
-    m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString();
-    m_bufferSize   = settings.value(QStringLiteral("BufferSize"), 128).toInt();
-    settings.endGroup();
+#ifdef RT_AUDIO
+    m_useRtAudio     = settings.value(QStringLiteral("Backend"), 0).toInt() == 1;
+    m_inputDevice    = settings.value(QStringLiteral("InputDevice"), "").toString();
+    m_outputDevice   = settings.value(QStringLiteral("OutputDevice"), "").toString();
+    m_bufferSize     = settings.value(QStringLiteral("BufferSize"), 128).toInt();
     m_previousBuffer = m_bufferSize;
     refreshDevices();
     m_previousInput  = m_inputDevice;
@@ -110,6 +127,8 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
         QStringLiteral("outputComboModel"),
         QVariant::fromValue(QStringList(QLatin1String(""))));
 #endif
+    m_bufferStrategy = settings.value(QStringLiteral("BufferStrategy"), 0).toInt();
+    settings.endGroup();
 
 #ifdef USE_WEAK_JACK
     // Check if Jack is available
@@ -129,6 +148,9 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
 
     m_view.engine()->rootContext()->setContextProperty(
         QStringLiteral("bufferComboModel"), QVariant::fromValue(m_bufferOptions));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("bufferStrategyComboModel"),
+        QVariant::fromValue(m_bufferStrategyOptions));
     m_view.engine()->rootContext()->setContextProperty(
         QStringLiteral("updateChannelComboModel"),
         QVariant::fromValue(m_updateChannelOptions));
@@ -136,6 +158,16 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
                                                        this);
     m_view.engine()->rootContext()->setContextProperty(QStringLiteral("serverModel"),
                                                        QVariant::fromValue(m_servers));
+
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("inputMeterModel"), QVariant::fromValue(QVector<float>()));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("outputMeterModel"), QVariant::fromValue(QVector<float>()));
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputClipped"),
+                                                       QVariant::fromValue(false));
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("outputClipped"),
+                                                       QVariant::fromValue(false));
+
     m_view.engine()->rootContext()->setContextProperty(
         QStringLiteral("backendComboModel"),
         QVariant::fromValue(QStringList()
@@ -162,8 +194,6 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
         sendHeartbeat();
     });
 
-    // Connect joinStudio callbacks
-    connect(this, &VirtualStudio::studioToJoinChanged, this, &VirtualStudio::joinStudio);
     // QueuedConnection since refreshFinished is sometimes signaled from a network reply
     // thread
     connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio,
@@ -308,6 +338,22 @@ void VirtualStudio::setBufferSize([[maybe_unused]] int index)
 #endif
 }
 
+int VirtualStudio::bufferStrategy()
+{
+    return m_bufferStrategy;
+}
+
+void VirtualStudio::setBufferStrategy(int index)
+{
+    m_bufferStrategy =
+        index >= 0 ? index
+                   : m_bufferStrategyOptions.indexOf(QStringLiteral("Minimal Latency"));
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("Audio"));
+    settings.setValue(QStringLiteral("BufferStrategy"), m_bufferStrategy);
+    settings.endGroup();
+}
+
 int VirtualStudio::currentStudio()
 {
     return m_currentStudio;
@@ -374,6 +420,17 @@ void VirtualStudio::setShowSelfHosted(bool selfHosted)
     settings.endGroup();
 }
 
+bool VirtualStudio::showCreateStudio()
+{
+    return m_showCreateStudio;
+}
+
+void VirtualStudio::setShowCreateStudio(bool createStudio)
+{
+    m_showCreateStudio = createStudio;
+    emit showCreateStudioChanged();
+}
+
 bool VirtualStudio::showDeviceSetup()
 {
     return m_showDeviceSetup;
@@ -402,6 +459,8 @@ void VirtualStudio::setShowWarnings(bool show)
         // device setup view proceeds warning view
         // if device setup is shown, do not immediately join
         if (!m_showDeviceSetup) {
+            // We're done waiting to be on the browse page
+            m_shouldJoin = true;
             joinStudio();
         }
     }
@@ -446,7 +505,6 @@ QUrl VirtualStudio::studioToJoin()
 void VirtualStudio::setStudioToJoin(const QUrl& url)
 {
     m_studioToJoin = url;
-    emit studioToJoinChanged();
 }
 
 bool VirtualStudio::noUpdater()
@@ -472,6 +530,16 @@ QString VirtualStudio::failedMessage()
     return m_failedMessage;
 }
 
+bool VirtualStudio::shouldJoin()
+{
+    return m_shouldJoin;
+}
+
+void VirtualStudio::setShouldJoin(bool join)
+{
+    m_shouldJoin = join;
+}
+
 void VirtualStudio::joinStudio()
 {
     if (!m_authenticated || m_studioToJoin.isEmpty() || m_servers.isEmpty()) {
@@ -479,11 +547,17 @@ void VirtualStudio::joinStudio()
         // getServerList emits refreshFinished which
         // will come back to this function.
         if (m_authenticated && !m_studioToJoin.isEmpty() && m_servers.isEmpty()) {
-            getServerList(true);
+            getServerList(true, true);
         }
         return;
     }
 
+    if (!m_shouldJoin) {
+        // Not time to join yet.
+        // Waiting until joinStudio is called and m_shouldJoin is true.
+        return;
+    }
+
     QString scheme = m_studioToJoin.scheme();
     QString path   = m_studioToJoin.path();
     QString url    = m_studioToJoin.toString();
@@ -583,9 +657,9 @@ void VirtualStudio::logout()
     emit hasRefreshTokenChanged();
 }
 
-void VirtualStudio::refreshStudios(int index)
+void VirtualStudio::refreshStudios(int index, bool signalRefresh)
 {
-    getServerList(false, index);
+    getServerList(false, signalRefresh, index);
 }
 
 void VirtualStudio::refreshDevices()
@@ -658,6 +732,8 @@ void VirtualStudio::applySettings()
     // this function is called after the device setup view
     // which can display upon opening the app from join link
     if (!m_studioToJoin.isEmpty()) {
+        // We're done waiting to be on the browse page
+        m_shouldJoin = true;
         joinStudio();
     }
 }
@@ -731,7 +807,7 @@ void VirtualStudio::completeConnection()
     }
 
     m_jackTripRunning = true;
-    m_connectionState = QStringLiteral("Connecting...");
+    m_connectionState = QStringLiteral("Preparing audio...");
     emit connectionStateChanged();
     VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
     try {
@@ -751,8 +827,8 @@ void VirtualStudio::completeConnection()
             buffer_size = m_bufferSize;
         }
 #endif
-        JackTrip* jackTrip =
-            m_device->initJackTrip(m_useRtAudio, input, output, buffer_size, studioInfo);
+        JackTrip* jackTrip = m_device->initJackTrip(
+            m_useRtAudio, input, output, buffer_size, m_bufferStrategy, studioInfo);
 
         QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this,
                          &VirtualStudio::processFinished, Qt::QueuedConnection);
@@ -762,7 +838,36 @@ void VirtualStudio::completeConnection()
                          &VirtualStudio::receivedConnectionFromPeer,
                          Qt::QueuedConnection);
 
+        Meter* m_outputMeter = new Meter(jackTrip->getNumOutputChannels());
+        jackTrip->appendProcessPluginFromNetwork(m_outputMeter);
+        connect(m_outputMeter, &Meter::onComputedVolumeMeasurements, this,
+                &VirtualStudio::updatedOutputVuMeasurements);
+
+        Meter* m_inputMeter = new Meter(jackTrip->getNumInputChannels());
+        jackTrip->appendProcessPluginToNetwork(m_inputMeter);
+        connect(m_inputMeter, &Meter::onComputedVolumeMeasurements, this,
+                &VirtualStudio::updatedInputVuMeasurements);
+
+        m_connectionState = QStringLiteral("Connecting...");
+        emit connectionStateChanged();
+#ifdef RT_AUDIO
+        if (m_useRtAudio) {
+            // This is a hack. RtAudio::openStream blocks the UI thread.
+            // But I am not comfortable changing how all of JackTrip consumes
+            // RtAudio to fix a VS mode bug.
+            delay(805);
+        }
+#endif
         m_device->startJackTrip();
+
+        m_view.engine()->rootContext()->setContextProperty(
+            QStringLiteral("inputMeterModel"),
+            QVariant::fromValue(QVector<float>(jackTrip->getNumInputChannels())));
+
+        m_view.engine()->rootContext()->setContextProperty(
+            QStringLiteral("outputMeterModel"),
+            QVariant::fromValue(QVector<float>(jackTrip->getNumOutputChannels())));
+
         m_device->startPinger(studioInfo);
     } catch (const std::exception& e) {
         // Let the user know what our exception was.
@@ -907,7 +1012,8 @@ void VirtualStudio::slotAuthSucceded()
     if (!m_studioToJoin.isEmpty()) {
         // FTUX shows warnings and device setup views
         // if any of these enabled, do not immediately join
-        if (!m_showWarnings && !m_showDeviceSetup) {
+        if (!m_showDeviceSetup) {
+            // Don't need to set m_shouldJoin because it's default true
             joinStudio();
         }
     }
@@ -1047,6 +1153,70 @@ void VirtualStudio::updatedStats(const QJsonObject& stats)
     return;
 }
 
+void VirtualStudio::updatedInputVuMeasurements(const QVector<float> valuesInDecibels)
+{
+    QJsonArray uiValues;
+    bool detectedClip = false;
+
+    // Always output 2 meter readings to the UI
+    for (int i = 0; i < 2; i++) {
+        // Determine decibel reading
+        float dB = m_meterMin;
+        if (i < valuesInDecibels.size()) {
+            dB = std::max(m_meterMin, valuesInDecibels[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        float meter = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+        QJsonObject object{{QStringLiteral("dB"), dB}, {QStringLiteral("level"), meter}};
+        uiValues.push_back(object);
+
+        // Signal a clip if we haven't done so already
+        if (dB >= -0.05 && !detectedClip) {
+            m_inputClipTimer.start();
+            m_view.engine()->rootContext()->setContextProperty(
+                QStringLiteral("inputClipped"), QVariant::fromValue(true));
+            detectedClip = true;
+        }
+    }
+
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputMeterModel"),
+                                                       QVariant::fromValue(uiValues));
+}
+
+void VirtualStudio::updatedOutputVuMeasurements(const QVector<float> valuesInDecibels)
+{
+    QJsonArray uiValues;
+    bool detectedClip = false;
+
+    // Always output 2 meter readings to the UI
+    for (int i = 0; i < 2; i++) {
+        // Determine decibel reading
+        float dB = m_meterMin;
+        if (i < valuesInDecibels.size()) {
+            dB = std::max(m_meterMin, valuesInDecibels[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        float meter = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+        QJsonObject object{{QStringLiteral("dB"), dB}, {QStringLiteral("level"), meter}};
+        uiValues.push_back(object);
+
+        // Signal a clip if we haven't done so already
+        if (dB >= -0.05 && !detectedClip) {
+            m_outputClipTimer.start();
+            m_view.engine()->rootContext()->setContextProperty(
+                QStringLiteral("outputClipped"), QVariant::fromValue(true));
+            detectedClip = true;
+        }
+    }
+
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("outputMeterModel"),
+                                                       QVariant::fromValue(uiValues));
+}
+
 void VirtualStudio::setupAuthenticator()
 {
     if (m_authenticator.isNull()) {
@@ -1100,12 +1270,13 @@ void VirtualStudio::setupAuthenticator()
 
 void VirtualStudio::sendHeartbeat()
 {
-    if (m_device != nullptr && m_connectionState != "Connecting...") {
+    if (m_device != nullptr && m_connectionState != "Connecting..."
+        && m_connectionState != "Preparing audio...") {
         m_device->sendHeartbeat();
     }
 }
 
-void VirtualStudio::getServerList(bool firstLoad, int index)
+void VirtualStudio::getServerList(bool firstLoad, bool signalRefresh, int index)
 {
     {
         QMutexLocker locker(&m_refreshMutex);
@@ -1124,149 +1295,172 @@ void VirtualStudio::getServerList(bool firstLoad, int index)
 
     QNetworkReply* reply =
         m_authenticator->get(QStringLiteral("https://app.jacktrip.org/api/servers"));
-    connect(reply, &QNetworkReply::finished, this, [&, reply, topServerId, firstLoad]() {
-        if (reply->error() != QNetworkReply::NoError) {
-            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
-            emit authFailed();
-            reply->deleteLater();
-            return;
-        }
+    connect(
+        reply, &QNetworkReply::finished, this,
+        [&, reply, topServerId, firstLoad, signalRefresh]() {
+            if (reply->error() != QNetworkReply::NoError) {
+                std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+                emit authFailed();
+                reply->deleteLater();
+                return;
+            }
 
-        QByteArray response      = reply->readAll();
-        QJsonDocument serverList = QJsonDocument::fromJson(response);
-        if (!serverList.isArray()) {
-            std::cout << "Error: Not an array" << std::endl;
-            QMutexLocker locker(&m_refreshMutex);
-            m_refreshInProgress = false;
-            emit authFailed();
-            reply->deleteLater();
-            return;
-        }
-        QJsonArray servers = serverList.array();
-        // Divide our servers by category initially so that they're easier to sort
-        QList<QObject*> yourServers;
-        QList<QObject*> subServers;
-        QList<QObject*> pubServers;
-
-        for (int i = 0; i < servers.count(); i++) {
-            if (servers.at(i)[QStringLiteral("type")].toString().contains(
-                    QStringLiteral("JackTrip"))) {
-                VsServerInfo* serverInfo = new VsServerInfo(this);
-                serverInfo->setIsManageable(
-                    servers.at(i)[QStringLiteral("admin")].toBool());
-                QString status    = servers.at(i)[QStringLiteral("status")].toString();
-                bool activeStudio = status == QLatin1String("Ready");
-                bool hostedStudio = servers.at(i)[QStringLiteral("managed")].toBool();
-                // Only iterate through servers that we want to show
-                if (!m_showSelfHosted && !hostedStudio) {
-                    continue;
-                }
-                if (!m_showInactive && !activeStudio) {
-                    continue;
-                }
-                if (activeStudio || (serverInfo->isManageable() && m_showInactive)) {
-                    serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString());
-                    serverInfo->setHost(
-                        servers.at(i)[QStringLiteral("serverHost")].toString());
-                    serverInfo->setStatus(
-                        servers.at(i)[QStringLiteral("status")].toString());
-                    serverInfo->setPort(
-                        servers.at(i)[QStringLiteral("serverPort")].toInt());
-                    serverInfo->setIsPublic(
-                        servers.at(i)[QStringLiteral("public")].toBool());
-                    serverInfo->setRegion(
-                        servers.at(i)[QStringLiteral("region")].toString());
-                    serverInfo->setPeriod(
-                        servers.at(i)[QStringLiteral("period")].toInt());
-                    serverInfo->setSampleRate(
-                        servers.at(i)[QStringLiteral("sampleRate")].toInt());
-                    serverInfo->setQueueBuffer(
-                        servers.at(i)[QStringLiteral("queueBuffer")].toInt());
-                    serverInfo->setBannerURL(
-                        servers.at(i)[QStringLiteral("bannerURL")].toString());
-                    serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString());
-                    serverInfo->setSessionId(
-                        servers.at(i)[QStringLiteral("sessionId")].toString());
-                    if (servers.at(i)[QStringLiteral("owner")].toBool()) {
-                        yourServers.append(serverInfo);
-                        serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
-                    } else if (m_subscribedServers.contains(serverInfo->id())) {
-                        subServers.append(serverInfo);
-                        serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
-                    } else {
-                        pubServers.append(serverInfo);
+            QByteArray response      = reply->readAll();
+            QJsonDocument serverList = QJsonDocument::fromJson(response);
+            if (!serverList.isArray()) {
+                std::cout << "Error: Not an array" << std::endl;
+                QMutexLocker locker(&m_refreshMutex);
+                m_refreshInProgress = false;
+                emit authFailed();
+                reply->deleteLater();
+                return;
+            }
+            QJsonArray servers = serverList.array();
+            // Divide our servers by category initially so that they're easier to sort
+            QList<QObject*> yourServers;
+            QList<QObject*> subServers;
+            QList<QObject*> pubServers;
+            int skippedStudios = 0;
+
+            for (int i = 0; i < servers.count(); i++) {
+                if (servers.at(i)[QStringLiteral("type")].toString().contains(
+                        QStringLiteral("JackTrip"))) {
+                    VsServerInfo* serverInfo = new VsServerInfo(this);
+                    serverInfo->setIsManageable(
+                        servers.at(i)[QStringLiteral("admin")].toBool());
+                    QString status = servers.at(i)[QStringLiteral("status")].toString();
+                    bool activeStudio = status == QLatin1String("Ready");
+                    bool hostedStudio = servers.at(i)[QStringLiteral("managed")].toBool();
+                    // Only iterate through servers that we want to show
+                    if (!m_showSelfHosted && !hostedStudio) {
+                        if (activeStudio || (serverInfo->isManageable())) {
+                            skippedStudios++;
+                        }
+                        continue;
+                    }
+                    if (!m_showInactive && !activeStudio) {
+                        if (serverInfo->isManageable()) {
+                            skippedStudios++;
+                        }
+                        continue;
+                    }
+                    if (activeStudio || (serverInfo->isManageable() && m_showInactive)) {
+                        serverInfo->setName(
+                            servers.at(i)[QStringLiteral("name")].toString());
+                        serverInfo->setHost(
+                            servers.at(i)[QStringLiteral("serverHost")].toString());
+                        serverInfo->setStatus(
+                            servers.at(i)[QStringLiteral("status")].toString());
+                        serverInfo->setPort(
+                            servers.at(i)[QStringLiteral("serverPort")].toInt());
+                        serverInfo->setIsPublic(
+                            servers.at(i)[QStringLiteral("public")].toBool());
+                        serverInfo->setRegion(
+                            servers.at(i)[QStringLiteral("region")].toString());
+                        serverInfo->setPeriod(
+                            servers.at(i)[QStringLiteral("period")].toInt());
+                        serverInfo->setSampleRate(
+                            servers.at(i)[QStringLiteral("sampleRate")].toInt());
+                        serverInfo->setQueueBuffer(
+                            servers.at(i)[QStringLiteral("queueBuffer")].toInt());
+                        serverInfo->setBannerURL(
+                            servers.at(i)[QStringLiteral("bannerURL")].toString());
+                        serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString());
+                        serverInfo->setSessionId(
+                            servers.at(i)[QStringLiteral("sessionId")].toString());
+                        serverInfo->setInviteKey(
+                            servers.at(i)[QStringLiteral("inviteKey")].toString());
+                        if (servers.at(i)[QStringLiteral("owner")].toBool()) {
+                            yourServers.append(serverInfo);
+                            serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
+                        } else if (m_subscribedServers.contains(serverInfo->id())) {
+                            subServers.append(serverInfo);
+                            serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
+                        } else {
+                            pubServers.append(serverInfo);
+                        }
                     }
                 }
             }
-        }
 
-        std::sort(yourServers.begin(), yourServers.end(),
-                  [](QObject* first, QObject* second) {
-                      return static_cast<VsServerInfo*>(first)->name()
-                             < static_cast<VsServerInfo*>(second)->name();
-                  });
-        std::sort(subServers.begin(), subServers.end(),
-                  [](QObject* first, QObject* second) {
-                      return static_cast<VsServerInfo*>(first)->name()
-                             < static_cast<VsServerInfo*>(second)->name();
-                  });
-        std::sort(pubServers.begin(), pubServers.end(),
-                  [](QObject* first, QObject* second) {
-                      return static_cast<VsServerInfo*>(first)->name()
-                             < static_cast<VsServerInfo*>(second)->name();
-                  });
-
-        // If we don't have any owned servers, move the JackTrip logo to an appropriate
-        // section header.
-        if (yourServers.isEmpty()) {
-            if (subServers.isEmpty()) {
-                m_logoSection = QStringLiteral("Public Studios");
+            std::sort(yourServers.begin(), yourServers.end(),
+                      [](QObject* first, QObject* second) {
+                          return static_cast<VsServerInfo*>(first)->name()
+                                 < static_cast<VsServerInfo*>(second)->name();
+                      });
+            std::sort(subServers.begin(), subServers.end(),
+                      [](QObject* first, QObject* second) {
+                          return static_cast<VsServerInfo*>(first)->name()
+                                 < static_cast<VsServerInfo*>(second)->name();
+                      });
+            std::sort(pubServers.begin(), pubServers.end(),
+                      [](QObject* first, QObject* second) {
+                          return static_cast<VsServerInfo*>(first)->name()
+                                 < static_cast<VsServerInfo*>(second)->name();
+                      });
+
+            // If we don't have any owned servers, move the JackTrip logo to an
+            // appropriate section header.
+            if (yourServers.isEmpty()) {
+                if (subServers.isEmpty()) {
+                    m_logoSection = QStringLiteral("Public Studios");
+
+                    if (pubServers.isEmpty() && skippedStudios == 0) {
+                        // This is a new user
+                        setShowCreateStudio(true);
+                    } else {
+                        // This is not a new user.
+                        // Set to false in case the studio created since refreshing.
+                        setShowCreateStudio(false);
+                    }
+                } else {
+                    m_logoSection = QStringLiteral("Subscribed Studios");
+                }
+                emit logoSectionChanged();
             } else {
-                m_logoSection = QStringLiteral("Subscribed Studios");
+                m_logoSection = QStringLiteral("Your Studios");
+                emit logoSectionChanged();
             }
-            emit logoSectionChanged();
-        } else {
-            m_logoSection = QStringLiteral("Your Studios");
-            emit logoSectionChanged();
-        }
 
-        QMutexLocker locker(&m_refreshMutex);
-        // Check that we haven't tried connecting to a server between the
-        // request going out and the response.
-        if (!m_allowRefresh) {
-            m_refreshInProgress = false;
-            return;
-        }
-        m_servers.clear();
-        m_servers.append(yourServers);
-        m_servers.append(subServers);
-        m_servers.append(pubServers);
-        m_view.engine()->rootContext()->setContextProperty(
-            QStringLiteral("serverModel"), QVariant::fromValue(m_servers));
-        int index = -1;
-        if (!topServerId.isEmpty()) {
-            for (int i = 0; i < m_servers.count(); i++) {
-                if (static_cast<VsServerInfo*>(m_servers.at(i))->id() == topServerId) {
-                    index = i;
-                    break;
+            QMutexLocker locker(&m_refreshMutex);
+            // Check that we haven't tried connecting to a server between the
+            // request going out and the response.
+            if (!m_allowRefresh) {
+                m_refreshInProgress = false;
+                return;
+            }
+            m_servers.clear();
+            m_servers.append(yourServers);
+            m_servers.append(subServers);
+            m_servers.append(pubServers);
+            m_view.engine()->rootContext()->setContextProperty(
+                QStringLiteral("serverModel"), QVariant::fromValue(m_servers));
+            int index = -1;
+            if (!topServerId.isEmpty()) {
+                for (int i = 0; i < m_servers.count(); i++) {
+                    if (static_cast<VsServerInfo*>(m_servers.at(i))->id()
+                        == topServerId) {
+                        index = i;
+                        break;
+                    }
                 }
             }
-        }
-        if (firstLoad) {
-            emit authSucceeded();
-            emit refreshFinished(index);
-            m_refreshTimer.setInterval(10000);
-            m_refreshTimer.start();
-            m_heartbeatTimer.setInterval(5000);
-            m_heartbeatTimer.start();
-        } else {
-            emit refreshFinished(index);
-        }
+            if (firstLoad) {
+                emit authSucceeded();
+                m_refreshTimer.setInterval(10000);
+                m_refreshTimer.start();
+                m_heartbeatTimer.setInterval(5000);
+                m_heartbeatTimer.start();
+            }
 
-        m_refreshInProgress = false;
+            if (signalRefresh) {
+                emit refreshFinished(index);
+            }
 
-        reply->deleteLater();
-    });
+            m_refreshInProgress = false;
+
+            reply->deleteLater();
+        });
 }
 
 void VirtualStudio::getUserId()
@@ -1320,7 +1514,7 @@ void VirtualStudio::getSubscriptions()
             m_subscribedServers.append(
                 subscriptions.at(i)[QStringLiteral("serverId")].toString());
         }
-        getServerList(true);
+        getServerList(true, false);
         reply->deleteLater();
     });
 }
@@ -1410,5 +1604,8 @@ VirtualStudio::~VirtualStudio()
         delete m_servers.at(i);
     }
 
+    delete m_inputMeter;
+    delete m_outputMeter;
+
     QDesktopServices::unsetUrlHandler("jacktrip");
 }
index 0d0d401701e680dcee65cff8b8ec61c7d4acc4ae..428ffa9fe5a88755a149d38908e4df8bc3fbc7e5 100644 (file)
 #ifndef VIRTUALSTUDIO_H
 #define VIRTUALSTUDIO_H
 
+#include <QEventLoop>
 #include <QList>
 #include <QMutex>
 #include <QScopedPointer>
 #include <QSharedPointer>
 #include <QTimer>
+#include <QVector>
 #include <QtNetworkAuth>
 
 #include "../JackTrip.h"
+#include "../Meter.h"
 #include "vsDevice.h"
 #include "vsQuickView.h"
 #include "vsServerInfo.h"
@@ -74,6 +77,8 @@ class VirtualStudio : public QObject
                    outputDeviceChanged)
     Q_PROPERTY(
         int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
+    Q_PROPERTY(int bufferStrategy READ bufferStrategy WRITE setBufferStrategy NOTIFY
+                   bufferStrategyChanged)
     Q_PROPERTY(int currentStudio READ currentStudio NOTIFY currentStudioChanged)
     Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged)
     Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged)
@@ -81,8 +86,11 @@ class VirtualStudio : public QObject
                    showInactiveChanged)
     Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY
                    showSelfHostedChanged)
+    Q_PROPERTY(bool showCreateStudio READ showCreateStudio WRITE setShowCreateStudio
+                   NOTIFY showCreateStudioChanged)
     Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged)
     Q_PROPERTY(QJsonObject networkStats READ networkStats NOTIFY networkStatsChanged)
+
     Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
                    updateChannelChanged)
     Q_PROPERTY(float fontScale READ fontScale CONSTANT)
@@ -95,6 +103,8 @@ class VirtualStudio : public QObject
     Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT)
     Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT)
     Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged)
+    Q_PROPERTY(
+        bool shouldJoin READ shouldJoin WRITE setShouldJoin NOTIFY shouldJoinChanged)
 
    public:
     explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr);
@@ -117,17 +127,23 @@ class VirtualStudio : public QObject
     void setOutputDevice(int device);
     int bufferSize();
     void setBufferSize(int index);
+    int bufferStrategy();
+    void setBufferStrategy(int index);
     int currentStudio();
     QJsonObject regions();
     QJsonObject userMetadata();
     QString connectionState();
     QJsonObject networkStats();
+    QVector<float> inputMeterLevels();
+    QVector<float> outputMeterLevels();
     QString updateChannel();
     void setUpdateChannel(const QString& channel);
     bool showInactive();
     void setShowInactive(bool inactive);
     bool showSelfHosted();
     void setShowSelfHosted(bool selfHosted);
+    bool showCreateStudio();
+    void setShowCreateStudio(bool createStudio);
     float fontScale();
     float uiScale();
     void setUiScale(float scale);
@@ -142,13 +158,15 @@ class VirtualStudio : public QObject
     bool noUpdater();
     bool psiBuild();
     QString failedMessage();
+    bool shouldJoin();
+    void setShouldJoin(bool join);
 
    public slots:
     void toStandard();
     void toVirtualStudio();
     void login();
     void logout();
-    void refreshStudios(int index);
+    void refreshStudios(int index, bool signalRefresh = false);
     void refreshDevices();
     void revertSettings();
     void applySettings();
@@ -175,11 +193,13 @@ class VirtualStudio : public QObject
     void inputDeviceChanged();
     void outputDeviceChanged();
     void bufferSizeChanged();
+    void bufferStrategyChanged();
     void currentStudioChanged();
     void regionsChanged();
     void userMetadataChanged();
     void showInactiveChanged();
     void showSelfHostedChanged();
+    void showCreateStudioChanged();
     void connectionStateChanged();
     void networkStatsChanged();
     void updateChannelChanged();
@@ -188,10 +208,10 @@ class VirtualStudio : public QObject
     void uiScaleChanged();
     void newScale();
     void darkModeChanged();
-    void studioToJoinChanged();
     void signalExit();
     void periodicRefresh();
     void failedMessageChanged();
+    void shouldJoinChanged();
 
    private slots:
     void slotAuthSucceded();
@@ -204,12 +224,15 @@ class VirtualStudio : public QObject
     void launchBrowser(const QUrl& url);
     void joinStudio();
     void updatedStats(const QJsonObject& stats);
+    void updatedInputVuMeasurements(const QVector<float> valuesInDecibels);
+    void updatedOutputVuMeasurements(const QVector<float> valuesInDecibels);
 
    private:
     void setupAuthenticator();
 
     void sendHeartbeat();
-    void getServerList(bool firstLoad = false, int index = -1);
+    void getServerList(bool firstLoad = false, bool signalRefresh = false,
+                       int index = -1);
     void getUserId();
     void getSubscriptions();
     void getRegions();
@@ -221,6 +244,7 @@ class VirtualStudio : public QObject
 
     bool m_showFirstRun = false;
     bool m_checkSsl     = true;
+    bool m_shouldJoin   = true;
     QString m_updateChannel;
     QString m_refreshToken;
     QString m_userId;
@@ -259,16 +283,26 @@ class VirtualStudio : public QObject
     bool m_isExiting         = false;
     bool m_showInactive      = false;
     bool m_showSelfHosted    = false;
+    bool m_showCreateStudio  = false;
     bool m_showDeviceSetup   = true;
     bool m_showWarnings      = true;
     float m_fontScale        = 1;
     float m_uiScale;
     float m_previousUiScale;
+    int m_bufferStrategy    = 0;
     bool m_darkMode         = false;
     QString m_failedMessage = "";
     QUrl m_studioToJoin;
     bool m_authenticated = false;
 
+    Meter* m_inputMeter;
+    Meter* m_outputMeter;
+    QTimer m_inputClipTimer;
+    QTimer m_outputClipTimer;
+
+    float m_meterMax = 0.0;
+    float m_meterMin = -64.0;
+
 #ifdef RT_AUDIO
     QStringList m_inputDeviceList;
     QStringList m_outputDeviceList;
@@ -279,9 +313,19 @@ class VirtualStudio : public QObject
     QString m_previousOutput;
     quint16 m_previousBuffer;
     bool m_previousUseRtAudio = false;
+    inline void delay(int millisecondsWait)
+    {
+        QEventLoop loop;
+        QTimer t;
+        t.connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit);
+        t.start(millisecondsWait);
+        loop.exec();
+    }
 #endif
-    QStringList m_bufferOptions        = {"16", "32", "64", "128", "256", "512", "1024"};
-    QStringList m_updateChannelOptions = {"Stable", "Edge"};
+    QStringList m_bufferOptions         = {"16", "32", "64", "128", "256", "512", "1024"};
+    QStringList m_bufferStrategyOptions = {"Minimal Latency", "Stable Latency",
+                                           "Loss Concealment"};
+    QStringList m_updateChannelOptions  = {"Stable", "Edge"};
 
 #ifdef __APPLE__
     NoNap m_noNap;
index 218520182307bd7c800e45cffbfd067a22398034..fec620bca7e8af917fbd840a42d1e78a7a4c9751 100644 (file)
@@ -127,8 +127,10 @@ Rectangle {
         target: virtualstudio
         onAuthSucceeded: { 
             if (virtualstudio.showDeviceSetup) {
+                virtualstudio.shouldJoin = false;
                 window.state = "setup";
             } else {
+                virtualstudio.shouldJoin = true;
                 window.state = "browse";
             }
         }
index cc4a673083618653b6529a6dc3f0e86636b5a756..0f1468fb4c28fe3907023100fe2fdb3b9f873b9c 100644 (file)
@@ -49,8 +49,6 @@ VsDevice::VsDevice(QOAuth2AuthorizationCodeFlow* authenticator, QObject* parent)
     m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString();
     m_appUUID   = settings.value(QStringLiteral("AppUUID"), "").toString();
     m_appID     = settings.value(QStringLiteral("AppID"), "").toString();
-
-    sendHeartbeat();
 }
 
 // registerApp idempotently registers an emulated device belonging to the current user
@@ -95,6 +93,8 @@ void VsDevice::registerApp()
                 reply->deleteLater();
                 return;
             }
+        } else if (m_apiPrefix != "" && m_apiSecret != "") {
+            sendHeartbeat();
         }
 
         QSettings settings;
@@ -258,6 +258,7 @@ JackTrip* VsDevice::initJackTrip([[maybe_unused]] bool useRtAudio,
                                  [[maybe_unused]] std::string input,
                                  [[maybe_unused]] std::string output,
                                  [[maybe_unused]] int bufferSize,
+                                 [[maybe_unused]] int bufferStrategy,
                                  VsServerInfo* studioInfo)
 {
     m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2,
@@ -276,7 +277,8 @@ JackTrip* VsDevice::initJackTrip([[maybe_unused]] bool useRtAudio,
     }
 #endif
     m_jackTrip->setRemoteClientName(m_appID);
-    m_jackTrip->setBufferStrategy(1);
+    // increment m_bufferStrategy by 1 for array-index mapping
+    m_jackTrip->setBufferStrategy(bufferStrategy + 1);
     m_jackTrip->setBufferQueueLength(-500);
     m_jackTrip->setPeerAddress(studioInfo->host());
     m_jackTrip->setPeerPorts(studioInfo->port());
@@ -454,6 +456,8 @@ void VsDevice::registerJTAsDevice()
             settings.beginGroup(QStringLiteral("VirtualStudio"));
             settings.setValue(QStringLiteral("AppID"), m_appID);
             settings.endGroup();
+
+            sendHeartbeat();
         }
 
         reply->deleteLater();
index bab61c78c7d2aaf35725b0b0506390a59b34581c..59b53412680db5c5e99acff34181b16d1f8e0c96 100644 (file)
@@ -65,7 +65,7 @@ class VsDevice : public QObject
     void sendHeartbeat();
     void setServerId(QString studioID);
     JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output,
-                           int bufferSize, VsServerInfo* studioInfo);
+                           int bufferSize, int bufferStrategy, VsServerInfo* studioInfo);
     void startJackTrip();
     void stopJackTrip();
     void reconcileAgentConfig(QJsonDocument newState);
diff --git a/src/gui/vsQmlClipboard.h b/src/gui/vsQmlClipboard.h
new file mode 100644 (file)
index 0000000..285b350
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef VSQMLCLIPBOARD_H
+#define VSQMLCLIPBOARD_H
+
+#include <QApplication>
+#include <QClipboard>
+#include <QObject>
+
+class VsQmlClipboard : public QObject
+{
+    Q_OBJECT
+   public:
+    explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent)
+    {
+        clipboard = QApplication::clipboard();
+    }
+
+    Q_INVOKABLE void setText(QString text)
+    {
+        clipboard->setText(text, QClipboard::Clipboard);
+    }
+
+   private:
+    QClipboard* clipboard;
+};
+
+#endif  // VSQMLCLIPBOARD_H
\ No newline at end of file
index c01a4b87f2fc0255f0c33c47f39ee88e6fca5383..7a2352a381a4150d16f0242af52a1debc95d775b 100644 (file)
@@ -225,4 +225,14 @@ void VsServerInfo::setSessionId(const QString& sessionId)
     m_sessionId = sessionId;
 }
 
+QString VsServerInfo::inviteKey()
+{
+    return m_inviteKey;
+}
+
+void VsServerInfo::setInviteKey(const QString& inviteKey)
+{
+    m_inviteKey = inviteKey;
+}
+
 VsServerInfo::~VsServerInfo() = default;
index 642bc6dbb440f3fc6b9c2833540ebeeea2f12990..9039ca731d0b3bd9032e0e2950226d289faafeac 100644 (file)
@@ -59,6 +59,8 @@ class VsServerInfo : public QObject
     Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT)
     Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT)
     Q_PROPERTY(QString status READ status CONSTANT)
+    Q_PROPERTY(QString id READ id CONSTANT)
+    Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT)
 
    public:
     enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS };
@@ -99,6 +101,8 @@ class VsServerInfo : public QObject
     void setSessionId(const QString& sessionId);
     QString status();
     void setStatus(const QString& status);
+    QString inviteKey();
+    void setInviteKey(const QString& inviteKey);
 
    signals:
     void canConnectChanged();
@@ -118,6 +122,7 @@ class VsServerInfo : public QObject
     QString m_id;
     QString m_sessionId;
     QString m_status;
+    QString m_inviteKey;
 
     /* Remaining JSON fields
     "loopback": true,
@@ -137,6 +142,7 @@ class VsServerInfo : public QObject
     "createdAt": "2021-09-07T17:15:38Z",
     "expiresAt": "2021-09-07T17:15:38Z",
     "updatedAt": "2021-09-07T17:15:38Z"
+    "inviteKey": "invitestring",
     */
 };
 
index f57e6ef806e4f6d4c91de08b5454b47e32f3f910..03f51c89f4622dd89bf8b1e50f349874495ea352 100644 (file)
@@ -40,7 +40,7 @@
 
 #include "AudioInterface.h"
 
-constexpr const char* const gVersion = "1.6.3";  ///< JackTrip version
+constexpr const char* const gVersion = "1.6.4";  ///< JackTrip version
 
 //*******************************************************************************
 /// \name Default Values
index 9b0cf482e11ebe43130b23df18a4db318db8bbff..80554d4ee0b17ad08a9863ce621f6c29f6122b02 100644 (file)
@@ -56,6 +56,7 @@
 
 #include "JTApplication.h"
 #include "gui/virtualstudio.h"
+#include "gui/vsQmlClipboard.h"
 #include "gui/vsUrlHandler.h"
 #endif
 
@@ -311,6 +312,9 @@ int main(int argc, char* argv[])
         }
 
 #ifndef NO_VS
+        // Register clipboard Qml type
+        qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
+
         // Parse command line for deep link
         QCommandLineOption deeplinkOption(QStringList() << QStringLiteral("deeplink"));
         deeplinkOption.setValueName(QStringLiteral("deeplink"));
diff --git a/src/meterdsp.h b/src/meterdsp.h
new file mode 100644 (file)
index 0000000..eb2faff
--- /dev/null
@@ -0,0 +1,1832 @@
+/* ------------------------------------------------------------
+author: "Dominick Hing"
+license: "MIT Style STK-4.2"
+name: "meter"
+version: "1.0"
+Code generated with Faust 2.40.0 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn meterdsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __METERDSP_H__
+#define __METERDSP_H__
+
+// NOTE: ANY INCLUDE-GUARD HERE MUST BE DERIVED FROM THE CLASS NAME
+//
+// faust2header.cpp - FAUST Architecture File
+// This is a simple variation of matlabplot.cpp in the Faust distribution
+// aimed at creating a simple C++ header file (.h) containing a Faust DSP.
+// See the Makefile for how to use it.
+
+/************************** BEGIN dsp.h ********************************
+ FAUST Architecture File
+ Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+ ---------------------------------------------------------------------
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ EXCEPTION : As a special exception, you may create a larger work
+ that contains this FAUST architecture section and distribute
+ that work under terms of your choice, so long as this FAUST
+ architecture section is not modified.
+ ************************************************************************/
+
+#ifndef __dsp__
+#define __dsp__
+
+#include <string>
+#include <vector>
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+struct UI;
+struct Meta;
+
+/**
+ * DSP memory manager.
+ */
+
+struct dsp_memory_manager {
+    virtual ~dsp_memory_manager() {}
+
+    /**
+     * Inform the Memory Manager with the number of expected memory zones.
+     * @param count - the number of expected memory zones
+     */
+    virtual void begin(size_t /*count*/) {}
+
+    /**
+     * Give the Memory Manager information on a given memory zone.
+     * @param size - the size in bytes of the memory zone
+     * @param reads - the number of Read access to the zone used to compute one frame
+     * @param writes - the number of Write access to the zone used to compute one frame
+     */
+    virtual void info(size_t /*size*/, size_t /*reads*/, size_t /*writes*/) {}
+
+    /**
+     * Inform the Memory Manager that all memory zones have been described,
+     * to possibly start a 'compute the best allocation strategy' step.
+     */
+    virtual void end() {}
+
+    /**
+     * Allocate a memory zone.
+     * @param size - the memory zone size in bytes
+     */
+    virtual void* allocate(size_t size) = 0;
+
+    /**
+     * Destroy a memory zone.
+     * @param ptr - the memory zone pointer to be deallocated
+     */
+    virtual void destroy(void* ptr) = 0;
+};
+
+/**
+ * Signal processor definition.
+ */
+
+class dsp
+{
+   public:
+    dsp() {}
+    virtual ~dsp() {}
+
+    /* Return instance number of audio inputs */
+    virtual int getNumInputs() = 0;
+
+    /* Return instance number of audio outputs */
+    virtual int getNumOutputs() = 0;
+
+    /**
+     * Trigger the ui_interface parameter with instance specific calls
+     * to 'openTabBox', 'addButton', 'addVerticalSlider'... in order to build the UI.
+     *
+     * @param ui_interface - the user interface builder
+     */
+    virtual void buildUserInterface(UI* ui_interface) = 0;
+
+    /* Return the sample rate currently used by the instance */
+    virtual int getSampleRate() = 0;
+
+    /**
+     * Global init, calls the following methods:
+     * - static class 'classInit': static tables initialization
+     * - 'instanceInit': constants and instance state initialization
+     *
+     * @param sample_rate - the sampling rate in Hz
+     */
+    virtual void init(int sample_rate) = 0;
+
+    /**
+     * Init instance state
+     *
+     * @param sample_rate - the sampling rate in Hz
+     */
+    virtual void instanceInit(int sample_rate) = 0;
+
+    /**
+     * Init instance constant state
+     *
+     * @param sample_rate - the sampling rate in Hz
+     */
+    virtual void instanceConstants(int sample_rate) = 0;
+
+    /* Init default control parameters values */
+    virtual void instanceResetUserInterface() = 0;
+
+    /* Init instance state (like delay lines...) but keep the control parameter values */
+    virtual void instanceClear() = 0;
+
+    /**
+     * Return a clone of the instance.
+     *
+     * @return a copy of the instance on success, otherwise a null pointer.
+     */
+    virtual dsp* clone() = 0;
+
+    /**
+     * Trigger the Meta* parameter with instance specific calls to 'declare' (key, value)
+     * metadata.
+     *
+     * @param m - the Meta* meta user
+     */
+    virtual void metadata(Meta* m) = 0;
+
+    /**
+     * DSP instance computation, to be called with successive in/out audio buffers.
+     *
+     * @param count - the number of frames to compute
+     * @param inputs - the input audio buffers as an array of non-interleaved FAUSTFLOAT
+     * samples (eiher float, double or quad)
+     * @param outputs - the output audio buffers as an array of non-interleaved FAUSTFLOAT
+     * samples (eiher float, double or quad)
+     *
+     */
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) = 0;
+
+    /**
+     * DSP instance computation: alternative method to be used by subclasses.
+     *
+     * @param date_usec - the timestamp in microsec given by audio driver.
+     * @param count - the number of frames to compute
+     * @param inputs - the input audio buffers as an array of non-interleaved FAUSTFLOAT
+     * samples (either float, double or quad)
+     * @param outputs - the output audio buffers as an array of non-interleaved FAUSTFLOAT
+     * samples (either float, double or quad)
+     *
+     */
+    virtual void compute(double /*date_usec*/, int count, FAUSTFLOAT** inputs,
+                         FAUSTFLOAT** outputs)
+    {
+        compute(count, inputs, outputs);
+    }
+};
+
+/**
+ * Generic DSP decorator.
+ */
+
+class decorator_dsp : public dsp
+{
+   protected:
+    dsp* fDSP;
+
+   public:
+    decorator_dsp(dsp* dsp = nullptr) : fDSP(dsp) {}
+    virtual ~decorator_dsp() { delete fDSP; }
+
+    virtual int getNumInputs() { return fDSP->getNumInputs(); }
+    virtual int getNumOutputs() { return fDSP->getNumOutputs(); }
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        fDSP->buildUserInterface(ui_interface);
+    }
+    virtual int getSampleRate() { return fDSP->getSampleRate(); }
+    virtual void init(int sample_rate) { fDSP->init(sample_rate); }
+    virtual void instanceInit(int sample_rate) { fDSP->instanceInit(sample_rate); }
+    virtual void instanceConstants(int sample_rate)
+    {
+        fDSP->instanceConstants(sample_rate);
+    }
+    virtual void instanceResetUserInterface() { fDSP->instanceResetUserInterface(); }
+    virtual void instanceClear() { fDSP->instanceClear(); }
+    virtual decorator_dsp* clone() { return new decorator_dsp(fDSP->clone()); }
+    virtual void metadata(Meta* m) { fDSP->metadata(m); }
+    // Beware: subclasses usually have to overload the two 'compute' methods
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        fDSP->compute(count, inputs, outputs);
+    }
+    virtual void compute(double date_usec, int count, FAUSTFLOAT** inputs,
+                         FAUSTFLOAT** outputs)
+    {
+        fDSP->compute(date_usec, count, inputs, outputs);
+    }
+};
+
+/**
+ * DSP factory class, used with LLVM and Interpreter backends
+ * to create DSP instances from a compiled DSP program.
+ */
+
+class dsp_factory
+{
+   protected:
+    // So that to force sub-classes to use deleteDSPFactory(dsp_factory* factory);
+    virtual ~dsp_factory() {}
+
+   public:
+    virtual std::string getName()                          = 0;
+    virtual std::string getSHAKey()                        = 0;
+    virtual std::string getDSPCode()                       = 0;
+    virtual std::string getCompileOptions()                = 0;
+    virtual std::vector<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> getIncludePathnames() = 0;
+
+    virtual dsp* createDSPInstance() = 0;
+
+    virtual void setMemoryManager(dsp_memory_manager* manager) = 0;
+    virtual dsp_memory_manager* getMemoryManager()             = 0;
+};
+
+// Denormal handling
+
+#if defined(__SSE__)
+#include <xmmintrin.h>
+#endif
+
+class ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#endif
+
+/************************** END dsp.h **************************/
+/************************** BEGIN APIUI.h *****************************
+FAUST Architecture File
+Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+---------------------------------------------------------------------
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+EXCEPTION : As a special exception, you may create a larger work
+that contains this FAUST architecture section and distribute
+that work under terms of your choice, so long as this FAUST
+architecture section is not modified.
+************************************************************************/
+
+#ifndef API_UI_H
+#define API_UI_H
+
+#include <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** BEGIN meta.h *******************************
+ FAUST Architecture File
+ Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+ ---------------------------------------------------------------------
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ EXCEPTION : As a special exception, you may create a larger work
+ that contains this FAUST architecture section and distribute
+ that work under terms of your choice, so long as this FAUST
+ architecture section is not modified.
+ ************************************************************************/
+
+#ifndef __meta__
+#define __meta__
+
+/**
+ The base class of Meta handler to be used in dsp::metadata(Meta* m) method to retrieve
+ (key, value) metadata.
+ */
+struct Meta {
+    virtual ~Meta() {}
+    virtual void declare(const char* key, const char* value) = 0;
+};
+
+#endif
+/**************************  END  meta.h **************************/
+/************************** BEGIN UI.h *****************************
+ FAUST Architecture File
+ Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+ ---------------------------------------------------------------------
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ EXCEPTION : As a special exception, you may create a larger work
+ that contains this FAUST architecture section and distribute
+ that work under terms of your choice, so long as this FAUST
+ architecture section is not modified.
+ ********************************************************************/
+
+#ifndef __UI_H__
+#define __UI_H__
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+/*******************************************************************************
+ * UI : Faust DSP User Interface
+ * User Interface as expected by the buildUserInterface() method of a DSP.
+ * This abstract class contains only the method that the Faust compiler can
+ * generate to describe a DSP user interface.
+ ******************************************************************************/
+
+struct Soundfile;
+
+template<typename REAL>
+struct UIReal {
+    UIReal() {}
+    virtual ~UIReal() {}
+
+    // -- widget's layouts
+
+    virtual void openTabBox(const char* label)        = 0;
+    virtual void openHorizontalBox(const char* label) = 0;
+    virtual void openVerticalBox(const char* label)   = 0;
+    virtual void closeBox()                           = 0;
+
+    // -- active widgets
+
+    virtual void addButton(const char* label, REAL* zone)      = 0;
+    virtual void addCheckButton(const char* label, REAL* zone) = 0;
+    virtual void addVerticalSlider(const char* label, REAL* zone, REAL init, REAL min,
+                                   REAL max, REAL step)        = 0;
+    virtual void addHorizontalSlider(const char* label, REAL* zone, REAL init, REAL min,
+                                     REAL max, REAL step)      = 0;
+    virtual void addNumEntry(const char* label, REAL* zone, REAL init, REAL min, REAL max,
+                             REAL step)                        = 0;
+
+    // -- passive widgets
+
+    virtual void addHorizontalBargraph(const char* label, REAL* zone, REAL min,
+                                       REAL max) = 0;
+    virtual void addVerticalBargraph(const char* label, REAL* zone, REAL min,
+                                     REAL max)   = 0;
+
+    // -- soundfiles
+
+    virtual void addSoundfile(const char* label, const char* filename,
+                              Soundfile** sf_zone) = 0;
+
+    // -- metadata declarations
+
+    virtual void declare(REAL* /*zone*/, const char* /*key*/, const char* /*val*/) {}
+
+    // To be used by LLVM client
+    virtual int sizeOfFAUSTFLOAT() { return sizeof(FAUSTFLOAT); }
+};
+
+struct UI : public UIReal<FAUSTFLOAT> {
+    UI() {}
+    virtual ~UI() {}
+};
+
+#endif
+/**************************  END  UI.h **************************/
+/************************** BEGIN PathBuilder.h **************************
+ FAUST Architecture File
+ Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+ ---------------------------------------------------------------------
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ EXCEPTION : As a special exception, you may create a larger work
+ that contains this FAUST architecture section and distribute
+ that work under terms of your choice, so long as this FAUST
+ architecture section is not modified.
+ ************************************************************************/
+
+#ifndef FAUST_PATHBUILDER_H
+#define FAUST_PATHBUILDER_H
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+
+   public:
+    PathBuilder() {}
+    virtual ~PathBuilder() {}
+
+    std::string replaceCharList(std::string str, const std::vector<char>& ch1, char ch2)
+    {
+        std::vector<char>::const_iterator beg = ch1.begin();
+        std::vector<char>::const_iterator end = ch1.end();
+        for (size_t i = 0; i < str.length(); ++i) {
+            if (std::find(beg, end, str[i]) != end) {
+                str[i] = ch2;
+            }
+        }
+        return str;
+    }
+
+    std::string buildPath(const std::string& label)
+    {
+        std::string res = "/";
+        for (size_t i = 0; i < fControlsLevel.size(); i++) {
+            res += fControlsLevel[i];
+            res += "/";
+        }
+        res += label;
+        std::vector<char> rep = {' ', '#', '*', ',', '/', '?',
+                                 '[', ']', '{', '}', '(', ')'};
+        replaceCharList(res, rep, '_');
+        return res;
+    }
+
+    void pushLabel(const std::string& label) { fControlsLevel.push_back(label); }
+    void popLabel() { fControlsLevel.pop_back(); }
+};
+
+#endif  // FAUST_PATHBUILDER_H
+/**************************  END  PathBuilder.h **************************/
+/************************** BEGIN ValueConverter.h ********************
+ FAUST Architecture File
+ Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale
+ ---------------------------------------------------------------------
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ EXCEPTION : As a special exception, you may create a larger work
+ that contains this FAUST architecture section and distribute
+ that work under terms of your choice, so long as this FAUST
+ architecture section is not modified.
+ ********************************************************************/
+
+#ifndef __ValueConverter__
+#define __ValueConverter__
+
+/***************************************************************************************
+ ValueConverter.h
+ (GRAME, Copyright 2015-2019)
+
+ Set of conversion objects used to map user interface values (for example a gui slider
+ delivering values between 0 and 1) to faust values (for example a vslider between
+ 20 and 20000) using a log scale.
+
+ -- Utilities
+
+ Range(lo,hi) : clip a value x between lo and hi
+ Interpolator(lo,hi,v1,v2) : Maps a value x between lo and hi to a value y between v1 and
+v2 Interpolator3pt(lo,mi,hi,v1,vm,v2) : Map values between lo mid hi to values between v1
+vm v2
+
+ -- Value Converters
+
+ ValueConverter::ui2faust(x)
+ ValueConverter::faust2ui(x)
+
+ -- ValueConverters used for sliders depending of the scale
+
+ LinearValueConverter(umin, umax, fmin, fmax)
+ LinearValueConverter2(lo, mi, hi, v1, vm, v2) using 2 segments
+ LogValueConverter(umin, umax, fmin, fmax)
+ ExpValueConverter(umin, umax, fmin, fmax)
+
+ -- ValueConverters used for accelerometers based on 3 points
+
+ AccUpConverter(amin, amid, amax, fmin, fmid, fmax)        -- curve 0
+ AccDownConverter(amin, amid, amax, fmin, fmid, fmax)      -- curve 1
+ AccUpDownConverter(amin, amid, amax, fmin, fmid, fmax)    -- curve 2
+ AccDownUpConverter(amin, amid, amax, fmin, fmid, fmax)    -- curve 3
+
+ -- lists of ZoneControl are used to implement accelerometers metadata for each axes
+
+ ZoneControl(zone, valueConverter) : a zone with an accelerometer data converter
+
+ -- ZoneReader are used to implement screencolor metadata
+
+ ZoneReader(zone, valueConverter) : a zone with a data converter
+
+****************************************************************************************/
+
+#include <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// Interpolator(lo,hi,v1,v2)
+// Maps a value x between lo and hi to a value y between v1 and v2
+// y = v1 + (x-lo)/(hi-lo)*(v2-v1)
+// y = v1 + (x-lo) * coef           with coef = (v2-v1)/(hi-lo)
+// y = v1 + x*coef - lo*coef
+// y = v1 - lo*coef + x*coef
+// y = offset + x*coef              with offset = v1 - lo*coef
+//--------------------------------------------------------------------------------------
+class Interpolator
+{
+   private:
+    //--------------------------------------------------------------------------------------
+    // Range(lo,hi) clip a value between lo and hi
+    //--------------------------------------------------------------------------------------
+    struct Range {
+        double fLo;
+        double fHi;
+
+        Range(double x, double y)
+            : fLo(std::min<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? fHi : x; }
+    };
+
+    Range fRange;
+    double fCoef;
+    double fOffset;
+
+   public:
+    Interpolator(double lo, double hi, double v1, double v2) : fRange(lo, hi)
+    {
+        if (hi != lo) {
+            // regular case
+            fCoef   = (v2 - v1) / (hi - lo);
+            fOffset = v1 - lo * fCoef;
+        } else {
+            // degenerate case, avoids division by zero
+            fCoef   = 0;
+            fOffset = (v1 + v2) / 2;
+        }
+    }
+    double operator()(double v)
+    {
+        double x = fRange(v);
+        return fOffset + x * fCoef;
+    }
+
+    void getLowHigh(double& amin, double& amax)
+    {
+        amin = fRange.fLo;
+        amax = fRange.fHi;
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Interpolator3pt(lo,mi,hi,v1,vm,v2)
+// Map values between lo mid hi to values between v1 vm v2
+//--------------------------------------------------------------------------------------
+class Interpolator3pt
+{
+   private:
+    Interpolator fSegment1;
+    Interpolator fSegment2;
+    double fMid;
+
+   public:
+    Interpolator3pt(double lo, double mi, double hi, double v1, double vm, double v2)
+        : fSegment1(lo, mi, v1, vm), fSegment2(mi, hi, vm, v2), fMid(mi)
+    {
+    }
+    double operator()(double x) { return (x < fMid) ? fSegment1(x) : fSegment2(x); }
+
+    void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fSegment1.getLowHigh(amin, amid);
+        fSegment2.getLowHigh(amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Abstract ValueConverter class. Converts values between UI and Faust representations
+//--------------------------------------------------------------------------------------
+class ValueConverter  // Identity by default
+{
+   public:
+    virtual ~ValueConverter() {}
+    virtual double ui2faust(double x) { return x; };
+    virtual double faust2ui(double x) { return x; };
+};
+
+//--------------------------------------------------------------------------------------
+// A converter than can be updated
+//--------------------------------------------------------------------------------------
+
+class UpdatableValueConverter : public ValueConverter
+{
+   protected:
+    bool fActive;
+
+   public:
+    UpdatableValueConverter() : fActive(true) {}
+    virtual ~UpdatableValueConverter() {}
+
+    virtual void setMappingValues(double amin, double amid, double amax, double min,
+                                  double init, double max)                  = 0;
+    virtual void getMappingValues(double& amin, double& amid, double& amax) = 0;
+
+    void setActive(bool on_off) { fActive = on_off; }
+    bool getActive() { return fActive; }
+};
+
+//--------------------------------------------------------------------------------------
+// Linear conversion between ui and Faust values
+//--------------------------------------------------------------------------------------
+class LinearValueConverter : public ValueConverter
+{
+   private:
+    Interpolator fUI2F;
+    Interpolator fF2UI;
+
+   public:
+    LinearValueConverter(double umin, double umax, double fmin, double fmax)
+        : fUI2F(umin, umax, fmin, fmax), fF2UI(fmin, fmax, umin, umax)
+    {
+    }
+
+    LinearValueConverter() : fUI2F(0., 0., 0., 0.), fF2UI(0., 0., 0., 0.) {}
+    virtual double ui2faust(double x) { return fUI2F(x); }
+    virtual double faust2ui(double x) { return fF2UI(x); }
+};
+
+//--------------------------------------------------------------------------------------
+// Two segments linear conversion between ui and Faust values
+//--------------------------------------------------------------------------------------
+class LinearValueConverter2 : public UpdatableValueConverter
+{
+   private:
+    Interpolator3pt fUI2F;
+    Interpolator3pt fF2UI;
+
+   public:
+    LinearValueConverter2(double amin, double amid, double amax, double min, double init,
+                          double max)
+        : fUI2F(amin, amid, amax, min, init, max), fF2UI(min, init, max, amin, amid, amax)
+    {
+    }
+
+    LinearValueConverter2() : fUI2F(0., 0., 0., 0., 0., 0.), fF2UI(0., 0., 0., 0., 0., 0.)
+    {
+    }
+
+    virtual double ui2faust(double x) { return fUI2F(x); }
+    virtual double faust2ui(double x) { return fF2UI(x); }
+
+    virtual void setMappingValues(double amin, double amid, double amax, double min,
+                                  double init, double max)
+    {
+        fUI2F = Interpolator3pt(amin, amid, amax, min, init, max);
+        fF2UI = Interpolator3pt(min, init, max, amin, amid, amax);
+    }
+
+    virtual void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fUI2F.getMappingValues(amin, amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Logarithmic conversion between ui and Faust values
+//--------------------------------------------------------------------------------------
+class LogValueConverter : public LinearValueConverter
+{
+   public:
+    LogValueConverter(double umin, double umax, double fmin, double fmax)
+        : LinearValueConverter(umin, umax, std::log(std::max<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(DBL_MIN, fmax)))
+    {
+    }
+
+    virtual double ui2faust(double x)
+    {
+        return std::exp(LinearValueConverter::ui2faust(x));
+    }
+    virtual double faust2ui(double x)
+    {
+        return LinearValueConverter::faust2ui(std::log(std::max<double>(x, DBL_MIN)));
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Exponential conversion between ui and Faust values
+//--------------------------------------------------------------------------------------
+class ExpValueConverter : public LinearValueConverter
+{
+   public:
+    ExpValueConverter(double umin, double umax, double fmin, double fmax)
+        : LinearValueConverter(umin, umax, std::min<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(DBL_MAX, std::exp(fmax)))
+    {
+    }
+
+    virtual double ui2faust(double x)
+    {
+        return std::log(LinearValueConverter::ui2faust(x));
+    }
+    virtual double faust2ui(double x)
+    {
+        return LinearValueConverter::faust2ui(std::min<double>(DBL_MAX, std::exp(x)));
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Convert accelerometer or gyroscope values to Faust values
+// Using an Up curve (curve 0)
+//--------------------------------------------------------------------------------------
+class AccUpConverter : public UpdatableValueConverter
+{
+   private:
+    Interpolator3pt fA2F;
+    Interpolator3pt fF2A;
+
+   public:
+    AccUpConverter(double amin, double amid, double amax, double fmin, double fmid,
+                   double fmax)
+        : fA2F(amin, amid, amax, fmin, fmid, fmax)
+        , fF2A(fmin, fmid, fmax, amin, amid, amax)
+    {
+    }
+
+    virtual double ui2faust(double x) { return fA2F(x); }
+    virtual double faust2ui(double x) { return fF2A(x); }
+
+    virtual void setMappingValues(double amin, double amid, double amax, double fmin,
+                                  double fmid, double fmax)
+    {
+        //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccUpConverter update %f %f %f
+        //%f %f %f", amin,amid,amax,fmin,fmid,fmax);
+        fA2F = Interpolator3pt(amin, amid, amax, fmin, fmid, fmax);
+        fF2A = Interpolator3pt(fmin, fmid, fmax, amin, amid, amax);
+    }
+
+    virtual void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fA2F.getMappingValues(amin, amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Convert accelerometer or gyroscope values to Faust values
+// Using a Down curve (curve 1)
+//--------------------------------------------------------------------------------------
+class AccDownConverter : public UpdatableValueConverter
+{
+   private:
+    Interpolator3pt fA2F;
+    Interpolator3pt fF2A;
+
+   public:
+    AccDownConverter(double amin, double amid, double amax, double fmin, double fmid,
+                     double fmax)
+        : fA2F(amin, amid, amax, fmax, fmid, fmin)
+        , fF2A(fmin, fmid, fmax, amax, amid, amin)
+    {
+    }
+
+    virtual double ui2faust(double x) { return fA2F(x); }
+    virtual double faust2ui(double x) { return fF2A(x); }
+
+    virtual void setMappingValues(double amin, double amid, double amax, double fmin,
+                                  double fmid, double fmax)
+    {
+        //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccDownConverter update %f %f
+        //%f %f %f %f", amin,amid,amax,fmin,fmid,fmax);
+        fA2F = Interpolator3pt(amin, amid, amax, fmax, fmid, fmin);
+        fF2A = Interpolator3pt(fmin, fmid, fmax, amax, amid, amin);
+    }
+
+    virtual void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fA2F.getMappingValues(amin, amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Convert accelerometer or gyroscope values to Faust values
+// Using an Up-Down curve (curve 2)
+//--------------------------------------------------------------------------------------
+class AccUpDownConverter : public UpdatableValueConverter
+{
+   private:
+    Interpolator3pt fA2F;
+    Interpolator fF2A;
+
+   public:
+    AccUpDownConverter(double amin, double amid, double amax, double fmin,
+                       double /*fmid*/, double fmax)
+        : fA2F(amin, amid, amax, fmin, fmax, fmin)
+        , fF2A(fmin, fmax, amin,
+               amax)  // Special, pseudo inverse of a non monotonic function
+    {
+    }
+
+    virtual double ui2faust(double x) { return fA2F(x); }
+    virtual double faust2ui(double x) { return fF2A(x); }
+
+    virtual void setMappingValues(double amin, double amid, double amax, double fmin,
+                                  double /*fmid*/, double fmax)
+    {
+        //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccUpDownConverter update %f %f
+        //%f %f %f %f", amin,amid,amax,fmin,fmid,fmax);
+        fA2F = Interpolator3pt(amin, amid, amax, fmin, fmax, fmin);
+        fF2A = Interpolator(fmin, fmax, amin, amax);
+    }
+
+    virtual void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fA2F.getMappingValues(amin, amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Convert accelerometer or gyroscope values to Faust values
+// Using a Down-Up curve (curve 3)
+//--------------------------------------------------------------------------------------
+class AccDownUpConverter : public UpdatableValueConverter
+{
+   private:
+    Interpolator3pt fA2F;
+    Interpolator fF2A;
+
+   public:
+    AccDownUpConverter(double amin, double amid, double amax, double fmin,
+                       double /*fmid*/, double fmax)
+        : fA2F(amin, amid, amax, fmax, fmin, fmax)
+        , fF2A(fmin, fmax, amin,
+               amax)  // Special, pseudo inverse of a non monotonic function
+    {
+    }
+
+    virtual double ui2faust(double x) { return fA2F(x); }
+    virtual double faust2ui(double x) { return fF2A(x); }
+
+    virtual void setMappingValues(double amin, double amid, double amax, double fmin,
+                                  double /*fmid*/, double fmax)
+    {
+        //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccDownUpConverter update %f %f
+        //%f %f %f %f", amin,amid,amax,fmin,fmid,fmax);
+        fA2F = Interpolator3pt(amin, amid, amax, fmax, fmin, fmax);
+        fF2A = Interpolator(fmin, fmax, amin, amax);
+    }
+
+    virtual void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fA2F.getMappingValues(amin, amid, amax);
+    }
+};
+
+//--------------------------------------------------------------------------------------
+// Base class for ZoneControl
+//--------------------------------------------------------------------------------------
+class ZoneControl
+{
+   protected:
+    FAUSTFLOAT* fZone;
+
+   public:
+    ZoneControl(FAUSTFLOAT* zone) : fZone(zone) {}
+    virtual ~ZoneControl() {}
+
+    virtual void update(double /*v*/) const {}
+
+    virtual void setMappingValues(int /*curve*/, double /*amin*/, double /*amid*/,
+                                  double /*amax*/, double /*min*/, double /*init*/,
+                                  double /*max*/)
+    {
+    }
+    virtual void getMappingValues(double& /*amin*/, double& /*amid*/, double& /*amax*/) {}
+
+    FAUSTFLOAT* getZone() { return fZone; }
+
+    virtual void setActive(bool /*on_off*/) {}
+    virtual bool getActive() { return false; }
+
+    virtual int getCurve() { return -1; }
+};
+
+//--------------------------------------------------------------------------------------
+//  Useful to implement accelerometers metadata as a list of ZoneControl for each axes
+//--------------------------------------------------------------------------------------
+class ConverterZoneControl : public ZoneControl
+{
+   protected:
+    ValueConverter* fValueConverter;
+
+   public:
+    ConverterZoneControl(FAUSTFLOAT* zone, ValueConverter* converter)
+        : ZoneControl(zone), fValueConverter(converter)
+    {
+    }
+    virtual ~ConverterZoneControl()
+    {
+        delete fValueConverter;
+    }  // Assuming fValueConverter is not kept elsewhere...
+
+    virtual void update(double v) const
+    {
+        *fZone = FAUSTFLOAT(fValueConverter->ui2faust(v));
+    }
+
+    ValueConverter* getConverter() { return fValueConverter; }
+};
+
+//--------------------------------------------------------------------------------------
+// Association of a zone and a four value converter, each one for each possible curve.
+// Useful to implement accelerometers metadata as a list of ZoneControl for each axes
+//--------------------------------------------------------------------------------------
+class CurveZoneControl : public ZoneControl
+{
+   private:
+    std::vector<UpdatableValueConverter*> fValueConverters;
+    int fCurve;
+
+   public:
+    CurveZoneControl(FAUSTFLOAT* zone, int curve, double amin, double amid, double amax,
+                     double min, double init, double max)
+        : ZoneControl(zone), fCurve(0)
+    {
+        assert(curve >= 0 && curve <= 3);
+        fValueConverters.push_back(new AccUpConverter(amin, amid, amax, min, init, max));
+        fValueConverters.push_back(
+            new AccDownConverter(amin, amid, amax, min, init, max));
+        fValueConverters.push_back(
+            new AccUpDownConverter(amin, amid, amax, min, init, max));
+        fValueConverters.push_back(
+            new AccDownUpConverter(amin, amid, amax, min, init, max));
+        fCurve = curve;
+    }
+    virtual ~CurveZoneControl()
+    {
+        for (const auto& it : fValueConverters) {
+            delete it;
+        }
+    }
+    void update(double v) const
+    {
+        if (fValueConverters[fCurve]->getActive())
+            *fZone = FAUSTFLOAT(fValueConverters[fCurve]->ui2faust(v));
+    }
+
+    void setMappingValues(int curve, double amin, double amid, double amax, double min,
+                          double init, double max)
+    {
+        fValueConverters[curve]->setMappingValues(amin, amid, amax, min, init, max);
+        fCurve = curve;
+    }
+
+    void getMappingValues(double& amin, double& amid, double& amax)
+    {
+        fValueConverters[fCurve]->getMappingValues(amin, amid, amax);
+    }
+
+    void setActive(bool on_off)
+    {
+        for (const auto& it : fValueConverters) {
+            it->setActive(on_off);
+        }
+    }
+
+    int getCurve() { return fCurve; }
+};
+
+class ZoneReader
+{
+   private:
+    FAUSTFLOAT* fZone;
+    Interpolator fInterpolator;
+
+   public:
+    ZoneReader(FAUSTFLOAT* zone, double lo, double hi)
+        : fZone(zone), fInterpolator(lo, hi, 0, 255)
+    {
+    }
+
+    virtual ~ZoneReader() {}
+
+    int getValue() { return (fZone != nullptr) ? int(fInterpolator(*fZone)) : 127; }
+};
+
+#endif
+/**************************  END  ValueConverter.h **************************/
+
+typedef unsigned int uint;
+
+class APIUI
+    : public PathBuilder
+    , public Meta
+    , public UI
+{
+   public:
+    enum ItemType {
+        kButton = 0,
+        kCheckButton,
+        kVSlider,
+        kHSlider,
+        kNumEntry,
+        kHBargraph,
+        kVBargraph
+    };
+    enum Type { kAcc = 0, kGyr = 1, kNoType };
+
+   protected:
+    enum Mapping { kLin = 0, kLog = 1, kExp = 2 };
+
+    struct Item {
+        std::string fPath;
+        std::string fLabel;
+        ValueConverter* fConversion;
+        FAUSTFLOAT* fZone;
+        FAUSTFLOAT fInit;
+        FAUSTFLOAT fMin;
+        FAUSTFLOAT fMax;
+        FAUSTFLOAT fStep;
+        ItemType fItemType;
+    };
+    std::vector<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> fGyr[3];
+
+    // Screen color control
+    // "...[screencolor:red]..." etc.
+    bool fHasScreenControl;  // true if control screen color metadata
+    ZoneReader* fRedReader;
+    ZoneReader* fGreenReader;
+    ZoneReader* fBlueReader;
+
+    // Current values controlled by metadata
+    std::string fCurrentUnit;
+    int fCurrentScale;
+    std::string fCurrentAcc;
+    std::string fCurrentGyr;
+    std::string fCurrentColor;
+    std::string fCurrentTooltip;
+    std::map<std::string, std::string> fCurrentMetadata;
+
+    // Add a generic parameter
+    virtual void addParameter(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init,
+                              FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step,
+                              ItemType type)
+    {
+        std::string path = buildPath(label);
+
+        // handle scale metadata
+        ValueConverter* converter = nullptr;
+        switch (fCurrentScale) {
+        case kLin:
+            converter = new LinearValueConverter(0, 1, min, max);
+            break;
+        case kLog:
+            converter = new LogValueConverter(0, 1, min, max);
+            break;
+        case kExp:
+            converter = new ExpValueConverter(0, 1, min, max);
+            break;
+        }
+        fCurrentScale = kLin;
+
+        fItems.push_back({path, label, converter, zone, init, min, max, step, type});
+
+        if (fCurrentAcc.size() > 0 && fCurrentGyr.size() > 0) {
+            fprintf(
+                stderr,
+                "warning : 'acc' and 'gyr' metadata used for the same %s parameter !!\n",
+                label);
+        }
+
+        // handle acc metadata "...[acc : <axe> <curve> <amin> <amid> <amax>]..."
+        if (fCurrentAcc.size() > 0) {
+            std::istringstream iss(fCurrentAcc);
+            int axe, curve;
+            double amin, amid, amax;
+            iss >> axe >> curve >> amin >> amid >> amax;
+
+            if ((0 <= axe) && (axe < 3) && (0 <= curve) && (curve < 4) && (amin < amax)
+                && (amin <= amid) && (amid <= amax)) {
+                fAcc[axe].push_back(
+                    new CurveZoneControl(zone, curve, amin, amid, amax, min, init, max));
+            } else {
+                fprintf(stderr, "incorrect acc metadata : %s \n", fCurrentAcc.c_str());
+            }
+            fCurrentAcc = "";
+        }
+
+        // handle gyr metadata "...[gyr : <axe> <curve> <amin> <amid> <amax>]..."
+        if (fCurrentGyr.size() > 0) {
+            std::istringstream iss(fCurrentGyr);
+            int axe, curve;
+            double amin, amid, amax;
+            iss >> axe >> curve >> amin >> amid >> amax;
+
+            if ((0 <= axe) && (axe < 3) && (0 <= curve) && (curve < 4) && (amin < amax)
+                && (amin <= amid) && (amid <= amax)) {
+                fGyr[axe].push_back(
+                    new CurveZoneControl(zone, curve, amin, amid, amax, min, init, max));
+            } else {
+                fprintf(stderr, "incorrect gyr metadata : %s \n", fCurrentGyr.c_str());
+            }
+            fCurrentGyr = "";
+        }
+
+        // handle screencolor metadata "...[screencolor:red|green|blue|white]..."
+        if (fCurrentColor.size() > 0) {
+            if ((fCurrentColor == "red") && (fRedReader == nullptr)) {
+                fRedReader        = new ZoneReader(zone, min, max);
+                fHasScreenControl = true;
+            } else if ((fCurrentColor == "green") && (fGreenReader == nullptr)) {
+                fGreenReader      = new ZoneReader(zone, min, max);
+                fHasScreenControl = true;
+            } else if ((fCurrentColor == "blue") && (fBlueReader == nullptr)) {
+                fBlueReader       = new ZoneReader(zone, min, max);
+                fHasScreenControl = true;
+            } else if ((fCurrentColor == "white") && (fRedReader == nullptr)
+                       && (fGreenReader == nullptr) && (fBlueReader == nullptr)) {
+                fRedReader        = new ZoneReader(zone, min, max);
+                fGreenReader      = new ZoneReader(zone, min, max);
+                fBlueReader       = new ZoneReader(zone, min, max);
+                fHasScreenControl = true;
+            } else {
+                fprintf(stderr, "incorrect screencolor metadata : %s \n",
+                        fCurrentColor.c_str());
+            }
+        }
+        fCurrentColor = "";
+
+        fMetaData.push_back(fCurrentMetadata);
+        fCurrentMetadata.clear();
+    }
+
+    int getZoneIndex(std::vector<ZoneControl*>* table, int p, int val)
+    {
+        FAUSTFLOAT* zone = fItems[uint(p)].fZone;
+        for (size_t i = 0; i < table[val].size(); i++) {
+            if (zone == table[val][i]->getZone())
+                return int(i);
+        }
+        return -1;
+    }
+
+    void setConverter(std::vector<ZoneControl*>* table, int p, int val, int curve,
+                      double amin, double amid, double amax)
+    {
+        int id1 = getZoneIndex(table, p, 0);
+        int id2 = getZoneIndex(table, p, 1);
+        int id3 = getZoneIndex(table, p, 2);
+
+        // Deactivates everywhere..
+        if (id1 != -1)
+            table[0][uint(id1)]->setActive(false);
+        if (id2 != -1)
+            table[1][uint(id2)]->setActive(false);
+        if (id3 != -1)
+            table[2][uint(id3)]->setActive(false);
+
+        if (val == -1) {  // Means: no more mapping...
+            // So stay all deactivated...
+        } else {
+            int id4 = getZoneIndex(table, p, val);
+            if (id4 != -1) {
+                // Reactivate the one we edit...
+                table[val][uint(id4)]->setMappingValues(
+                    curve, amin, amid, amax, fItems[uint(p)].fMin, fItems[uint(p)].fInit,
+                    fItems[uint(p)].fMax);
+                table[val][uint(id4)]->setActive(true);
+            } else {
+                // Allocate a new CurveZoneControl which is 'active' by default
+                FAUSTFLOAT* zone = fItems[uint(p)].fZone;
+                table[val].push_back(new CurveZoneControl(
+                    zone, curve, amin, amid, amax, fItems[uint(p)].fMin,
+                    fItems[uint(p)].fInit, fItems[uint(p)].fMax));
+            }
+        }
+    }
+
+    void getConverter(std::vector<ZoneControl*>* table, int p, int& val, int& curve,
+                      double& amin, double& amid, double& amax)
+    {
+        int id1 = getZoneIndex(table, p, 0);
+        int id2 = getZoneIndex(table, p, 1);
+        int id3 = getZoneIndex(table, p, 2);
+
+        if (id1 != -1) {
+            val   = 0;
+            curve = table[val][uint(id1)]->getCurve();
+            table[val][uint(id1)]->getMappingValues(amin, amid, amax);
+        } else if (id2 != -1) {
+            val   = 1;
+            curve = table[val][uint(id2)]->getCurve();
+            table[val][uint(id2)]->getMappingValues(amin, amid, amax);
+        } else if (id3 != -1) {
+            val   = 2;
+            curve = table[val][uint(id3)]->getCurve();
+            table[val][uint(id3)]->getMappingValues(amin, amid, amax);
+        } else {
+            val   = -1;  // No mapping
+            curve = 0;
+            amin  = -100.;
+            amid  = 0.;
+            amax  = 100.;
+        }
+    }
+
+   public:
+    APIUI()
+        : fHasScreenControl(false)
+        , fRedReader(nullptr)
+        , fGreenReader(nullptr)
+        , fBlueReader(nullptr)
+        , fCurrentScale(kLin)
+    {
+    }
+
+    virtual ~APIUI()
+    {
+        for (const auto& it : fItems)
+            delete it.fConversion;
+        for (int i = 0; i < 3; i++) {
+            for (const auto& it : fAcc[i])
+                delete it;
+            for (const auto& it : fGyr[i])
+                delete it;
+        }
+        delete fRedReader;
+        delete fGreenReader;
+        delete fBlueReader;
+    }
+
+    // -- widget's layouts
+
+    virtual void openTabBox(const char* label) { pushLabel(label); }
+    virtual void openHorizontalBox(const char* label) { pushLabel(label); }
+    virtual void openVerticalBox(const char* label) { pushLabel(label); }
+    virtual void closeBox() { popLabel(); }
+
+    // -- active widgets
+
+    virtual void addButton(const char* label, FAUSTFLOAT* zone)
+    {
+        addParameter(label, zone, 0, 0, 1, 1, kButton);
+    }
+
+    virtual void addCheckButton(const char* label, FAUSTFLOAT* zone)
+    {
+        addParameter(label, zone, 0, 0, 1, 1, kCheckButton);
+    }
+
+    virtual void addVerticalSlider(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init,
+                                   FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step)
+    {
+        addParameter(label, zone, init, min, max, step, kVSlider);
+    }
+
+    virtual void addHorizontalSlider(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init,
+                                     FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step)
+    {
+        addParameter(label, zone, init, min, max, step, kHSlider);
+    }
+
+    virtual void addNumEntry(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init,
+                             FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step)
+    {
+        addParameter(label, zone, init, min, max, step, kNumEntry);
+    }
+
+    // -- passive widgets
+
+    virtual void addHorizontalBargraph(const char* label, FAUSTFLOAT* zone,
+                                       FAUSTFLOAT min, FAUSTFLOAT max)
+    {
+        addParameter(label, zone, min, min, max, (max - min) / 1000.0f, kHBargraph);
+    }
+
+    virtual void addVerticalBargraph(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min,
+                                     FAUSTFLOAT max)
+    {
+        addParameter(label, zone, min, min, max, (max - min) / 1000.0f, kVBargraph);
+    }
+
+    // -- soundfiles
+
+    virtual void addSoundfile(const char* /*label*/, const char* /*filename*/,
+                              Soundfile** /*sf_zone*/)
+    {
+    }
+
+    // -- metadata declarations
+
+    virtual void declare(FAUSTFLOAT* /*zone*/, const char* key, const char* val)
+    {
+        // Keep metadata
+        fCurrentMetadata[key] = val;
+
+        if (strcmp(key, "scale") == 0) {
+            if (strcmp(val, "log") == 0) {
+                fCurrentScale = kLog;
+            } else if (strcmp(val, "exp") == 0) {
+                fCurrentScale = kExp;
+            } else {
+                fCurrentScale = kLin;
+            }
+        } else if (strcmp(key, "unit") == 0) {
+            fCurrentUnit = val;
+        } else if (strcmp(key, "acc") == 0) {
+            fCurrentAcc = val;
+        } else if (strcmp(key, "gyr") == 0) {
+            fCurrentGyr = val;
+        } else if (strcmp(key, "screencolor") == 0) {
+            fCurrentColor = val;  // val = "red", "green", "blue" or "white"
+        } else if (strcmp(key, "tooltip") == 0) {
+            fCurrentTooltip = val;
+        }
+    }
+
+    virtual void declare(const char* /*key*/, const char* /*val*/) {}
+
+    //-------------------------------------------------------------------------------
+    // Simple API part
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path)
+    {
+        auto it1 = find_if(fItems.begin(), fItems.end(), [=](const Item& it) {
+            return it.fPath == std::string(path);
+        });
+        if (it1 != fItems.end()) {
+            return int(it1 - fItems.begin());
+        }
+
+        auto it2 = find_if(fItems.begin(), fItems.end(), [=](const Item& it) {
+            return it.fLabel == std::string(path);
+        });
+        if (it2 != fItems.end()) {
+            return int(it2 - fItems.begin());
+        }
+
+        return -1;
+    }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        return (index >= 0) ? getParamValue(index) : FAUSTFLOAT(0);
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    void setParamValue(const char* path, FAUSTFLOAT v)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            setParamValue(index, v);
+        } else {
+            fprintf(stderr, "setParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+        }
+    }
+
+    double getParamRatio(int p)
+    {
+        return fItems[uint(p)].fConversion->faust2ui(*fItems[uint(p)].fZone);
+    }
+    void setParamRatio(int p, double r)
+    {
+        *fItems[uint(p)].fZone = FAUSTFLOAT(fItems[uint(p)].fConversion->ui2faust(r));
+    }
+
+    double value2ratio(int p, double r)
+    {
+        return fItems[uint(p)].fConversion->faust2ui(r);
+    }
+    double ratio2value(int p, double r)
+    {
+        return fItems[uint(p)].fConversion->ui2faust(r);
+    }
+
+    /**
+     * Return the control type (kAcc, kGyr, or -1) for a given parameter.
+     *
+     * @param p - the UI parameter index
+     *
+     * @return the type
+     */
+    Type getParamType(int p)
+    {
+        if (p >= 0) {
+            if (getZoneIndex(fAcc, p, 0) != -1 || getZoneIndex(fAcc, p, 1) != -1
+                || getZoneIndex(fAcc, p, 2) != -1) {
+                return kAcc;
+            } else if (getZoneIndex(fGyr, p, 0) != -1 || getZoneIndex(fGyr, p, 1) != -1
+                       || getZoneIndex(fGyr, p, 2) != -1) {
+                return kGyr;
+            }
+        }
+        return kNoType;
+    }
+
+    /**
+     * Return the Item type (kButton = 0, kCheckButton, kVSlider, kHSlider, kNumEntry,
+     * kHBargraph, kVBargraph) for a given parameter.
+     *
+     * @param p - the UI parameter index
+     *
+     * @return the Item type
+     */
+    ItemType getParamItemType(int p) { return fItems[uint(p)].fItemType; }
+
+    /**
+     * Set a new value coming from an accelerometer, propagate it to all relevant
+     * FAUSTFLOAT* zones.
+     *
+     * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer
+     * @param value - the new value
+     *
+     */
+    void propagateAcc(int acc, double value)
+    {
+        for (size_t i = 0; i < fAcc[acc].size(); i++) {
+            fAcc[acc][i]->update(value);
+        }
+    }
+
+    /**
+     * Used to edit accelerometer curves and mapping. Set curve and related mapping for a
+     * given UI parameter.
+     *
+     * @param p - the UI parameter index
+     * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer
+     * (-1 means "no mapping")
+     * @param curve - between 0 and 3
+     * @param amin - mapping 'min' point
+     * @param amid - mapping 'middle' point
+     * @param amax - mapping 'max' point
+     *
+     */
+    void setAccConverter(int p, int acc, int curve, double amin, double amid, double amax)
+    {
+        setConverter(fAcc, p, acc, curve, amin, amid, amax);
+    }
+
+    /**
+     * Used to edit gyroscope curves and mapping. Set curve and related mapping for a
+     * given UI parameter.
+     *
+     * @param p - the UI parameter index
+     * @param acc - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope (-1 means "no
+     * mapping")
+     * @param curve - between 0 and 3
+     * @param amin - mapping 'min' point
+     * @param amid - mapping 'middle' point
+     * @param amax - mapping 'max' point
+     *
+     */
+    void setGyrConverter(int p, int gyr, int curve, double amin, double amid, double amax)
+    {
+        setConverter(fGyr, p, gyr, curve, amin, amid, amax);
+    }
+
+    /**
+     * Used to edit accelerometer curves and mapping. Get curve and related mapping for a
+     * given UI parameter.
+     *
+     * @param p - the UI parameter index
+     * @param acc - the acc value to be retrieved (-1 means "no mapping")
+     * @param curve - the curve value to be retrieved
+     * @param amin - the amin value to be retrieved
+     * @param amid - the amid value to be retrieved
+     * @param amax - the amax value to be retrieved
+     *
+     */
+    void getAccConverter(int p, int& acc, int& curve, double& amin, double& amid,
+                         double& amax)
+    {
+        getConverter(fAcc, p, acc, curve, amin, amid, amax);
+    }
+
+    /**
+     * Used to edit gyroscope curves and mapping. Get curve and related mapping for a
+     * given UI parameter.
+     *
+     * @param p - the UI parameter index
+     * @param gyr - the gyr value to be retrieved (-1 means "no mapping")
+     * @param curve - the curve value to be retrieved
+     * @param amin - the amin value to be retrieved
+     * @param amid - the amid value to be retrieved
+     * @param amax - the amax value to be retrieved
+     *
+     */
+    void getGyrConverter(int p, int& gyr, int& curve, double& amin, double& amid,
+                         double& amax)
+    {
+        getConverter(fGyr, p, gyr, curve, amin, amid, amax);
+    }
+
+    /**
+     * Set a new value coming from an gyroscope, propagate it to all relevant FAUSTFLOAT*
+     * zones.
+     *
+     * @param gyr - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope
+     * @param value - the new value
+     *
+     */
+    void propagateGyr(int gyr, double value)
+    {
+        for (size_t i = 0; i < fGyr[gyr].size(); i++) {
+            fGyr[gyr][i]->update(value);
+        }
+    }
+
+    /**
+     * Get the number of FAUSTFLOAT* zones controlled with the accelerometer.
+     *
+     * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer
+     * @return the number of zones
+     *
+     */
+    int getAccCount(int acc) { return (acc >= 0 && acc < 3) ? int(fAcc[acc].size()) : 0; }
+
+    /**
+     * Get the number of FAUSTFLOAT* zones controlled with the gyroscope.
+     *
+     * @param gyr - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope
+     * @param the number of zones
+     *
+     */
+    int getGyrCount(int gyr) { return (gyr >= 0 && gyr < 3) ? int(fGyr[gyr].size()) : 0; }
+
+    // getScreenColor() : -1 means no screen color control (no screencolor metadata found)
+    // otherwise return 0x00RRGGBB a ready to use color
+    int getScreenColor()
+    {
+        if (fHasScreenControl) {
+            int r = (fRedReader) ? fRedReader->getValue() : 0;
+            int g = (fGreenReader) ? fGreenReader->getValue() : 0;
+            int b = (fBlueReader) ? fBlueReader->getValue() : 0;
+            return (r << 16) | (g << 8) | b;
+        } else {
+            return -1;
+        }
+    }
+};
+
+#endif
+/**************************  END  APIUI.h **************************/
+
+// NOTE: "faust -scn name" changes the last line above to
+// #include <faust/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+/* link with : "" */
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS meterdsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class meterdsp : public dsp
+{
+   private:
+    int fSampleRate;
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("author", "Dominick Hing");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.5");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn meterdsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("description", "VU Meter Faust Plugin for JackTrip");
+        m->declare("filename", "meterdsp.dsp");
+        m->declare("license", "MIT Style STK-4.2");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "meter");
+        m->declare("version", "1.0");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate) { fSampleRate = sample_rate; }
+
+    virtual void instanceResetUserInterface() {}
+
+    virtual void instanceClear() {}
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual meterdsp* clone() { return new meterdsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("meter");
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0 = float(input0[i0]);
+            float fTemp1 =
+                20.0f
+                * std::log10(std::max<float>(1.17549435e-38f,
+                                             std::max<float>(9.99999975e-05f, fTemp0)));
+            float fTemp2 = 100.0f * float(copysignf(float(fTemp1), 1.0f));
+            output0[i0]  = FAUSTFLOAT(float(copysignf(
+                 float(0.00999999978f
+                      * float(int(fTemp2) + (fTemp2 - std::floor(fTemp2) >= 0.5f))),
+                 float(fTemp1))));
+        }
+    }
+};
+
+#endif