From: IOhannes m zmölnig (Debian/GNU) Date: Tue, 20 Sep 2022 09:49:46 +0000 (+0200) Subject: New upstream version 1.6.4+ds0 X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1~1^2~9^2~17 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=9520db29015cf87c9c07755aa00a17b61e911471;p=jacktrip.git New upstream version 1.6.4+ds0 --- diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb1697..81717bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/docs/Build/Meson_build.md b/docs/Build/Meson_build.md index 5ea4037..b416f7d 100644 --- a/docs/Build/Meson_build.md +++ b/docs/Build/Meson_build.md @@ -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" diff --git a/docs/changelog.yml b/docs/changelog.yml index fd026c3..b424738 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -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 index 0000000..95f37da --- /dev/null +++ b/faust-src/meterdsp.dsp @@ -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, _); +}; diff --git a/jacktrip.pro b/jacktrip.pro index 15cda35..80fc9c1 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -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 \ diff --git a/meson.build b/meson.build index af9e535..8bb8b9f 100644 --- a/meson.build +++ b/meson.build @@ -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') diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json index 7b26f5f..2622246 100644 --- a/releases/edge/mac-manifests.json +++ b/releases/edge/mac-manifests.json @@ -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", diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json index 6304bbb..2331018 100644 --- a/releases/edge/win-manifests.json +++ b/releases/edge/win-manifests.json @@ -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", diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json index c91d622..49cf17d 100644 --- a/releases/stable/mac-manifests.json +++ b/releases/stable/mac-manifests.json @@ -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", diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json index 48486bb..74cb51d 100644 --- a/releases/stable/win-manifests.json +++ b/releases/stable/win-manifests.json @@ -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", diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index a6dc55b..1244aaa 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -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); } } diff --git a/src/JackAudioInterface.cpp b/src/JackAudioInterface.cpp index 5625435..516e9b9 100644 --- a/src/JackAudioInterface.cpp +++ b/src/JackAudioInterface.cpp @@ -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); } //******************************************************************************* diff --git a/src/JackAudioInterface.h b/src/JackAudioInterface.h index a3f6633..81ee103 100644 --- a/src/JackAudioInterface.h +++ b/src/JackAudioInterface.h @@ -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. */ diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index 5031beb..9d21357 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -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 diff --git a/src/JackTrip.h b/src/JackTrip.h index fa877d0..99cb23a 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -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 index 0000000..a58f8b6 --- /dev/null +++ b/src/Meter.cpp @@ -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 + +#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::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 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::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::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::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 index 0000000..a475dc0 --- /dev/null +++ b/src/Meter.h @@ -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 +#include +#include +#include +#include + +#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 meterP; + bool hasProcessedAudio = false; + + QTimer mTimer; + QVector mValues; + + private slots: + void onTick(); + + signals: + void onComputedVolumeMeasurements(QVector values); +}; + +#endif \ No newline at end of file diff --git a/src/ProcessPlugin.h b/src/ProcessPlugin.h index 18e99b3..d0aeab3 100644 --- a/src/ProcessPlugin.h +++ b/src/ProcessPlugin.h @@ -38,6 +38,7 @@ #ifndef __PROCESSPLUGIN_H__ #define __PROCESSPLUGIN_H__ +#include #include /** \brief Interface for the process plugins to add to the JACK callback process in @@ -48,8 +49,10 @@ * 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 diff --git a/src/Regulator.cpp b/src/Regulator.cpp index 24a6c7d..56a950a 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -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; } diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index 0b221c2..3c8a62e 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -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 { diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h index 51b6e9d..1397e68 100644 --- a/src/RtAudioInterface.h +++ b/src/RtAudioInterface.h @@ -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 diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml index c4558ff..d36870c 100644 --- a/src/gui/Browse.qml +++ b/src/gui/Browse.qml @@ -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 { diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml index 29ffbe5..2f9f1b4 100644 --- a/src/gui/Connected.qml +++ b/src/gui/Connected.qml @@ -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] = "" + minRtt + " ms - " + maxRtt + " ms, 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 " + quality + "." + 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: "Input Device" + 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: "Output Device" + 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] = "" + minRtt + " ms - " + maxRtt + " ms, 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 " + quality + "." - return texts; + Text { + id: networkStatsHeaderText + text: "Network" + 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 index 0000000..f3a3a3d --- /dev/null +++ b/src/gui/Meter.qml @@ -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 index 0000000..a996d0e --- /dev/null +++ b/src/gui/SectionHeading.qml @@ -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 diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml index 02a6f3d..45d75b5 100644 --- a/src/gui/Settings.qml +++ b/src/gui/Settings.qml @@ -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 { diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml index b087cc5..9f07b58 100644 --- a/src/gui/Studio.qml +++ b/src/gui/Studio.qml @@ -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 diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index 6a5678c..2861744 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -11,9 +11,11 @@ Studio.qml Browse.qml Settings.qml + Meter.qml Connected.qml Failed.qml Setup.qml + SectionHeading.qml logo.svg wedge.svg wedge_inactive.svg @@ -22,6 +24,7 @@ join.svg leave.svg manage.svg + share.svg cog.svg mic.svg ethernet.png diff --git a/src/gui/share.svg b/src/gui/share.svg new file mode 100644 index 0000000..98a1ea2 --- /dev/null +++ b/src/gui/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp index edc79d5..b2ccf52 100644 --- a/src/gui/virtualstudio.cpp +++ b/src/gui/virtualstudio.cpp @@ -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())); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputMeterModel"), QVariant::fromValue(QVector())); + 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(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(jackTrip->getNumInputChannels()))); + + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputMeterModel"), + QVariant::fromValue(QVector(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 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 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 yourServers; - QList subServers; - QList 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 yourServers; + QList subServers; + QList 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(first)->name() - < static_cast(second)->name(); - }); - std::sort(subServers.begin(), subServers.end(), - [](QObject* first, QObject* second) { - return static_cast(first)->name() - < static_cast(second)->name(); - }); - std::sort(pubServers.begin(), pubServers.end(), - [](QObject* first, QObject* second) { - return static_cast(first)->name() - < static_cast(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(first)->name() + < static_cast(second)->name(); + }); + std::sort(subServers.begin(), subServers.end(), + [](QObject* first, QObject* second) { + return static_cast(first)->name() + < static_cast(second)->name(); + }); + std::sort(pubServers.begin(), pubServers.end(), + [](QObject* first, QObject* second) { + return static_cast(first)->name() + < static_cast(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(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(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"); } diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h index 0d0d401..428ffa9 100644 --- a/src/gui/virtualstudio.h +++ b/src/gui/virtualstudio.h @@ -38,14 +38,17 @@ #ifndef VIRTUALSTUDIO_H #define VIRTUALSTUDIO_H +#include #include #include #include #include #include +#include #include #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 inputMeterLevels(); + QVector 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 valuesInDecibels); + void updatedOutputVuMeasurements(const QVector 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; diff --git a/src/gui/vs.qml b/src/gui/vs.qml index 2185201..fec620b 100644 --- a/src/gui/vs.qml +++ b/src/gui/vs.qml @@ -127,8 +127,10 @@ Rectangle { target: virtualstudio onAuthSucceeded: { if (virtualstudio.showDeviceSetup) { + virtualstudio.shouldJoin = false; window.state = "setup"; } else { + virtualstudio.shouldJoin = true; window.state = "browse"; } } diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp index cc4a673..0f1468f 100644 --- a/src/gui/vsDevice.cpp +++ b/src/gui/vsDevice.cpp @@ -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(); diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h index bab61c7..59b5341 100644 --- a/src/gui/vsDevice.h +++ b/src/gui/vsDevice.h @@ -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 index 0000000..285b350 --- /dev/null +++ b/src/gui/vsQmlClipboard.h @@ -0,0 +1,26 @@ +#ifndef VSQMLCLIPBOARD_H +#define VSQMLCLIPBOARD_H + +#include +#include +#include + +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 diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp index c01a4b8..7a2352a 100644 --- a/src/gui/vsServerInfo.cpp +++ b/src/gui/vsServerInfo.cpp @@ -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; diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h index 642bc6d..9039ca7 100644 --- a/src/gui/vsServerInfo.h +++ b/src/gui/vsServerInfo.h @@ -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", */ }; diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index f57e6ef..03f51c8 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 9b0cf48..80554d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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("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 index 0000000..eb2faff --- /dev/null +++ b/src/meterdsp.h @@ -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 +#include + +#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 getLibraryList() = 0; + virtual std::vector 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 +#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(fpsr_aux)); +#endif + } + + void getFpStatusRegister() noexcept + { +#if defined(__arm64__) || defined(__aarch64__) + asm volatile("mrs %0, fpcr" : "=r"(fpsr)); +#elif defined(__SSE__) + fpsr = static_cast(_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 + +#include +#include +#include +#include + +/************************** 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 +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 { + 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 +#include +#include + +/******************************************************************************* + * PathBuilder : Faust User Interface + * Helper class to build complete hierarchical path for UI items. + ******************************************************************************/ + +class PathBuilder +{ + protected: + std::vector fControlsLevel; + + public: + PathBuilder() {} + virtual ~PathBuilder() {} + + std::string replaceCharList(std::string str, const std::vector& ch1, char ch2) + { + std::vector::const_iterator beg = ch1.begin(); + std::vector::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 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 +#include + +#include // std::max +#include +#include + +//-------------------------------------------------------------------------------------- +// 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(x, y)), fHi(std::max(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(DBL_MIN, fmin)), + std::log(std::max(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(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(DBL_MAX, std::exp(fmin)), + std::min(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(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 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 fItems; + + std::vector > fMetaData; + std::vector fAcc[3]; + std::vector 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 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 : ]..." + 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 : ]..." + 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* 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* 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* 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 getMetadata(int p) + { + std::map res; + std::map 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 Generated Code +//---------------------------------------------------------------------------- + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +/* link with : "" */ +#include + +#include +#include +#include + +#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(1.17549435e-38f, + std::max(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