From: IOhannes m zmölnig (Debian/GNU) Date: Fri, 10 Nov 2023 11:11:30 +0000 (+0100) Subject: New upstream version 2.1.0+ds X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1~1^2~9^2~7 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=8fba864116fa42a3a5022beb02c3df72199a64b1;p=jacktrip.git New upstream version 2.1.0+ds --- diff --git a/LICENSE.md b/LICENSE.md index 39b5c1f..513cb6c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -16,6 +16,9 @@ JackTrip uses Qt library throughout the project so the resulting binaries are also subject to Qt's license. The builds provided on GitHub's Releases page use open source distribution of Qt, licensed under LGPL. +Using JackTrip to join Virtual Studios on Windows computers may use AVC (h264) +video encoders and decoders subject to the AVC Patent Portfolio License. + The text of individual licenses is provided in the `LICENSES/` folder. Qt's source code can be downloaded from [https://download.qt.io/official_releases/qt/](https://download.qt.io/official_releases/qt/). diff --git a/LICENSES/AVC.txt b/LICENSES/AVC.txt new file mode 100644 index 0000000..66cff0e --- /dev/null +++ b/LICENSES/AVC.txt @@ -0,0 +1,10 @@ +THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO +LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES +IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE +VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”) +AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A +CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS +OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC +VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR +ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED +FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM \ No newline at end of file diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 9205872..c8b54b0 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -25,7 +25,7 @@ dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2 ### Fedora (Qt6) ```sh -dnf install qt6-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel qt6-qtwebengine-devel qt6-qtwebchannel-devel qt6-qt5compat-devel +dnf install qt6-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel qt6-qtwebengine-devel qt6-qtwebchannel-devel qt6-qt5compat-devel qt6-qtshadertools-devel dnf groupinstall "C Development Tools and Libraries" dnf groupinstall "Development Tools" dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2 @@ -43,7 +43,7 @@ apt install qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt ### Ubuntu and Debian/Raspbian (Qt6) ```sh apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man libclang-dev libdbus-1-dev libdbus-1-dev python3-jinja2 -apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libgl1-mesa-dev +apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libqt6core5compat6-dev libqt6shadertools6-dev libgl1-mesa-dev # for GUI builds apt install libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev apt install librtaudio-dev # if building with RtAudio diff --git a/docs/changelog.yml b/docs/changelog.yml index e61bd72..1caca7b 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,15 @@ +- Version: "2.1.0" + Date: 2023-11-06 + Description: + - (added) VS Mode ability to create studios without a web browser + - (added) VS Mode improved network stability notifications + - (added) VS Mode dialog when QML plugins are missing + - (updated) VS Mode video improvements on Windows + - (updated) Packet loss concealment latency and quality improvements + - (fixed) Packet loss concealment glitches when buffer sizes don't match + - (fixed) VS Mode ensure that the app is disconnected at startup + - (fixed) Invalid escape sequence in Linux desktop file + - (fixed) VS Mode unable to change update channel - Version: "2.0.2" Date: 2023-09-01 Description: diff --git a/linux/README.md b/linux/README.md index f2e3bfe..fb8f267 100644 --- a/linux/README.md +++ b/linux/README.md @@ -13,7 +13,7 @@ dnf install -y qt6-qtbase qt6-qtbase-common qt6-qtbase-gui qt6-qtsvg qt6-qtwebso For Debian or Ubuntu: ``` -apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine +apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window ``` To install JackTrip as a Linux desktop application: diff --git a/linux/org.jacktrip.JackTrip.desktop.in b/linux/org.jacktrip.JackTrip.desktop.in index 620a3e6..c190cb0 100644 --- a/linux/org.jacktrip.JackTrip.desktop.in +++ b/linux/org.jacktrip.JackTrip.desktop.in @@ -3,7 +3,7 @@ Type=Application Name=JackTrip@name_suffix@ Comment=Network Music Performance over the Internet Comment[fr]=Performance de musique en réseau sur internet -Exec=/bin/bash -c "if [ -z ""\$"1" ]; then jacktrip; else jacktrip --gui --deeplink ""\$"1"; fi" /bin/bash %u +Exec=jacktrip %u Icon=@icon@ Terminal=false StartupWMClass=@wmclass@ diff --git a/meson.build b/meson.build index 7b58a64..d79bd43 100644 --- a/meson.build +++ b/meson.build @@ -208,11 +208,7 @@ else defines += '-DVS_FTUX' endif - if qt_version == '5' - deps += dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'QuickControls2', 'Qml', 'Svg', 'WebSockets', 'WebChannel'], include_type: 'system') - else - deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'QuickControls2', 'Qml', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system') - endif + deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Core5Compat', 'Quick', 'QuickControls2', 'Qml', 'ShaderTools', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system') qres = ['src/gui/qjacktrip.qrc'] endif diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json index c9fdcc6..b7b759e 100644 --- a/releases/edge/mac-manifests.json +++ b/releases/edge/mac-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "2.0.2", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2", + "download": { + "date": "2023-09-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-macOS-x64-signed-installer.pkg", + "downloadSize": "177283016", + "sha256": "e4cd66790d0d008980402aef42d712fe42788a87e9d48a28c49d5e9c6ba55aea" + } + }, { "version": "2.0.1", "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1", diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json index 7bc7e86..58f7b0a 100644 --- a/releases/edge/win-manifests.json +++ b/releases/edge/win-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "2.0.2", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2", + "download": { + "date": "2023-09-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Windows-x64-signed-installer.msi", + "downloadSize": "95850496", + "sha256": "6d06a9843f223fe95a76de838a442ce64050a1aeea7e64d096a496921e1711f1" + } + }, { "version": "2.0.1", "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1", diff --git a/releases/stable/linux-manifests.json b/releases/stable/linux-manifests.json index 955cfbc..c21757e 100644 --- a/releases/stable/linux-manifests.json +++ b/releases/stable/linux-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "2.0.2", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2", + "download": { + "date": "2023-09-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Linux-x64-binary.zip", + "downloadSize": "1234376", + "sha256": "1382d61c49af64082d36ceac953b23dbe5913dcb1a09a0dd60fb93a6837e319a" + } + }, { "version": "2.0.1", "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1", diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json index 72b0e18..569c766 100644 --- a/releases/stable/mac-manifests.json +++ b/releases/stable/mac-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "2.0.2", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2", + "download": { + "date": "2023-09-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-macOS-x64-signed-installer.pkg", + "downloadSize": "177283016", + "sha256": "e4cd66790d0d008980402aef42d712fe42788a87e9d48a28c49d5e9c6ba55aea" + } + }, { "version": "2.0.1", "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1", diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json index 019d672..41462eb 100644 --- a/releases/stable/win-manifests.json +++ b/releases/stable/win-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "2.0.2", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2", + "download": { + "date": "2023-09-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Windows-x64-signed-installer.msi", + "downloadSize": "95850496", + "sha256": "6d06a9843f223fe95a76de838a442ce64050a1aeea7e64d096a496921e1711f1" + } + }, { "version": "2.0.1", "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1", diff --git a/src/Analyzer.cpp b/src/Analyzer.cpp index dcd9f99..a87dff3 100644 --- a/src/Analyzer.cpp +++ b/src/Analyzer.cpp @@ -165,7 +165,17 @@ void Analyzer::onTick() // check for audio feedback loops bool detectedFeedback = checkForAudioFeedback(); + + // use mDetectionHistory to aggregate number of consecutive feedback triggers to help + // with false positives if (detectedFeedback) { + mDetectionHistory++; + } else { + if (mDetectionHistory > 0) { + mDetectionHistory--; + } + } + if (mDetectionHistory > 1) { emit signalFeedbackDetected(); } } @@ -207,19 +217,8 @@ void Analyzer::updateSpectraDifferentials() //******************************************************************************* bool Analyzer::checkForAudioFeedback() { - if (!testSpectralPeakAboveThreshold()) { - return false; - } - - if (!testSpectralPeakAbnormallyHigh()) { - return false; - } - - if (!testSpectralPeakGrowing()) { - return false; - } - - return true; + return testSpectralPeakAboveThreshold() && testSpectralPeakAbnormallyHigh() + && testSpectralPeakGrowing(); } //******************************************************************************* diff --git a/src/Analyzer.h b/src/Analyzer.h index 71e12d0..8c4e6fe 100644 --- a/src/Analyzer.h +++ b/src/Analyzer.h @@ -116,6 +116,7 @@ class Analyzer : public ProcessPlugin int mNumSpectra = 10; float** mSpectra = nullptr; float** mSpectraDifferentials = nullptr; + uint32_t mDetectionHistory = 0; signals: void signalFeedbackDetected(); diff --git a/src/DataProtocol.cpp b/src/DataProtocol.cpp index 93c458f..bea9d59 100644 --- a/src/DataProtocol.cpp +++ b/src/DataProtocol.cpp @@ -51,7 +51,7 @@ using std::endl; //******************************************************************************* DataProtocol::DataProtocol(JackTrip* jacktrip, const runModeT runmode, int /*bind_port*/, int /*peer_port*/) - : mStopped(false) + : mStopped(true) , mHasPacketsToReceive(false) , mRunMode(runmode) , mJackTrip(jacktrip) @@ -61,3 +61,20 @@ DataProtocol::DataProtocol(JackTrip* jacktrip, const runModeT runmode, int /*bin //******************************************************************************* DataProtocol::~DataProtocol() {} + +//******************************************************************************* +void DataProtocol::threadHasStarted() +{ + QMutexLocker lock(&mMutex); + mStopped = false; + mThreadHasStarted.notify_all(); +} + +//******************************************************************************* +void DataProtocol::waitForStart() +{ + QMutexLocker lock(&mMutex); + while (mStopped) { + mThreadHasStarted.wait(&mMutex); + } +} diff --git a/src/DataProtocol.h b/src/DataProtocol.h index 38755b6..c4c4032 100644 --- a/src/DataProtocol.h +++ b/src/DataProtocol.h @@ -54,6 +54,7 @@ #include #include #include +#include #include class JackTrip; // forward declaration @@ -183,6 +184,9 @@ class DataProtocol : public QThread } void setUseRtPriority(bool use) { mUseRtPriority = use; } + /// @brief wait for the thread to start up + void waitForStart(); + signals: void signalError(const char* error_message); @@ -195,6 +199,9 @@ class DataProtocol : public QThread */ runModeT getRunMode() const { return mRunMode; } + /// @brief called by the thread during startup + void threadHasStarted(); + /// Boolean stop the execution of the thread volatile bool mStopped; /// Boolean to indicate if the RECEIVER is waiting to obtain peer address @@ -202,6 +209,7 @@ class DataProtocol : public QThread /// Boolean that indicates if a packet was received volatile bool mHasPacketsToReceive; QMutex mMutex; + QWaitCondition mThreadHasStarted; private: int mLocalPort; ///< Local Port number to Bind diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index bf69b35..2e8b2b7 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -658,18 +658,17 @@ void JackTrip::completeConnection() std::cout << " JackTrip:startProcess before mDataProtocolReceiver->start" << std::endl; mDataProtocolReceiver->start(); - QThread::msleep(1); + mDataProtocolReceiver->waitForStart(); if (gVerboseFlag) std::cout << " JackTrip:startProcess before mDataProtocolSender->start" << std::endl; mDataProtocolSender->start(); + mDataProtocolSender->waitForStart(); /* * changed order so that audio starts after receiver and sender * because UdpDataProtocol:run0 before setRealtimeProcessPriority() * causes an audio hiccup from jack JackPosixSemaphore::TimedWait err = Interrupted - * system call new QThread::msleep(1); to allow sender to start */ - QThread::msleep(1); if (gVerboseFlag) std::cout << "step 5" << std::endl; if (gVerboseFlag) diff --git a/src/Regulator.cpp b/src/Regulator.cpp index e01dd56..9b83c28 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -91,16 +91,16 @@ using std::endl; using std::setw; // constants... -constexpr int HIST = 4; // for mono at FPP 16-128, see below for > mono, > 128 -constexpr int ModSeqNumInit = 256; // bounds on seqnums, 65536 is max in packet header -constexpr int NumSlotsMax = 128; // mNumSlots looped for recent arrivals -constexpr int LostWindowMax = 32; // mLostWindow looped for recent arrivals +constexpr int HIST = 4; // for mono at FPP 16-128, see below for > mono, > 128 +constexpr int NumSlotsMax = 4096; // mNumSlots looped for recent arrivals constexpr double DefaultAutoHeadroom = 3.0; // msec padding for auto adjusting mMsecTolerance constexpr double AutoMax = 250.0; // msec bounds on insane IPI, like ethernet unplugged constexpr double AutoInitDur = 6000.0; // kick in auto after this many msec constexpr double AutoInitValFactor = 0.5; // scale for initial mMsecTolerance during init phase if unspecified +constexpr double MaxWaitTime = 30; // msec + // tweak constexpr int WindowDivisor = 8; // for faster auto tracking constexpr int MaxFPP = 1024; // tested up to this FPP @@ -193,25 +193,24 @@ Regulator::Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen, mLastSeqNumIn.store(-1, std::memory_order_relaxed); mLastSeqNumOut = -1; mPhasor.resize(mNumChannels, 0.0); - mIncomingTiming.resize(ModSeqNumInit); - for (int i = 0; i < ModSeqNumInit; i++) + mIncomingTiming.resize(NumSlotsMax); + mAssemblyCounts.resize(NumSlotsMax); + for (int i = 0; i < NumSlotsMax; i++) { mIncomingTiming[i] = 0.0; - mModSeqNum = mNumSlots * 2; + mAssemblyCounts[i] = 0; + } mFPPratioNumerator = 1; mFPPratioDenominator = 1; mFPPratioIsSet = false; mBytesPeerPacket = mBytes; - mAssemblyCnt = 0; - mModCycle = 1; - mModSeqNumPeer = 1; mPeerFPP = mFPP; // use local until first packet arrives mAutoHeadroom = DefaultAutoHeadroom; - mFPPdurMsec = 1000.0 * mFPP / 48000.0; - changeGlobal_3(LostWindowMax); + mFPPdurMsec = 1000.0 * mFPP / mSampleRate; changeGlobal_2(NumSlotsMax); // need hg if running GUI if (m_b_BroadcastQueueLength) { - m_b_BroadcastRingBuffer = new JitterBuffer( - mFPP, qLen, 48000, 1, m_b_BroadcastQueueLength, mNumChannels, mAudioBitRes); + m_b_BroadcastRingBuffer = + new JitterBuffer(mFPP, qLen, mSampleRate, 1, m_b_BroadcastQueueLength, + mNumChannels, mAudioBitRes); qDebug() << "Broadcast started in Regulator with packet queue of" << m_b_BroadcastQueueLength; // have not implemented the mJackTrip->queueLengthChanged functionality @@ -250,19 +249,11 @@ void Regulator::changeGlobal_2(int x) mNumSlots = 1; if (mNumSlots > NumSlotsMax) mNumSlots = NumSlotsMax; - mModSeqNum = mNumSlots * 2; - printParams(); -} - -void Regulator::changeGlobal_3(int x) -{ // mLostWindow - mLostWindow = x; printParams(); } void Regulator::printParams(){ - // qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots - // << "mModSeqNum" << mModSeqNum << "mLostWindow" << mLostWindow; + // qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots; }; Regulator::~Regulator() @@ -299,13 +290,6 @@ void Regulator::setFPPratio() // qDebug() << "peerBuffers / localBuffers" << mFPPratioNumerator << " / " // << mFPPratioDenominator; } - if (mFPPratioNumerator > 1) { - mBytesPeerPacket = mBytes / mFPPratioNumerator; - mModCycle = mFPPratioNumerator - 1; - mModSeqNumPeer = mModSeqNum * mFPPratioNumerator; - } else if (mFPPratioDenominator > 1) { - mModSeqNumPeer = mModSeqNum / mFPPratioDenominator; - } } //******************************************************************************* @@ -313,23 +297,22 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) { if (seq_num != -1) { if (!mFPPratioIsSet) { // first peer packet - mPeerFPP = len / (mNumChannels * mBitResolutionMode); - mPeerFPPdurMsec = 1000.0 * mPeerFPP / 48000.0; + mBytesPeerPacket = len; + mPeerFPP = len / (mNumChannels * mBitResolutionMode); + mPeerFPPdurMsec = 1000.0 * mPeerFPP / mSampleRate; // bufstrategy 1 autoq mode overloads qLen with negative val // creates this ugly code - if (mMsecTolerance < 0) { // handle -q auto or, for example, -q auto10 + if (mMsecTolerance <= 0) { // handle -q auto or, for example, -q auto10 mAuto = true; // default is -500 from bufstrategy 1 autoq mode - // tweak - if (mMsecTolerance != -500.0) { - // use it to set headroom - mAutoHeadroom = -mMsecTolerance; - qDebug() << "PLC is in auto mode and has been set with" - << mAutoHeadroom << "ms headroom"; - if (mAutoHeadroom > 50.0) - qDebug() << "That's a very large value and should be less than, " - "for example, 50ms"; - } + // use mMsecTolerance to set headroom + mAutoHeadroom = + (mMsecTolerance == -500.0) ? DefaultAutoHeadroom : -mMsecTolerance; + qDebug() << "PLC is in auto mode and has been set with" << mAutoHeadroom + << "ms headroom"; + if (mAutoHeadroom > 50.0) + qDebug() << "That's a very large value and should be less than, " + "for example, 50ms"; // found an interesting relationship between mPeerFPP and initial // mMsecTolerance mPeerFPP*0.5 is pretty good though that's an oddball // conversion of bufsize directly to msec @@ -338,44 +321,36 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) setFPPratio(); // number of stats tick calls per sec depends on FPP int maxFPP = (mPeerFPP > mFPP) ? mPeerFPP : mFPP; - pushStat = - new StdDev(1, &mIncomingTimer, (int)(floor(48000.0 / (double)maxFPP))); + pushStat = new StdDev(1, &mIncomingTimer, + (int)(floor(mSampleRate / (double)maxFPP))); pullStat = - new StdDev(2, &mIncomingTimer, (int)(floor(48000.0 / (double)mFPP))); + new StdDev(2, &mIncomingTimer, (int)(floor(mSampleRate / (double)mFPP))); mFPPratioIsSet = true; } if (mFPPratioNumerator == mFPPratioDenominator) { + // local FPP matches peer pushPacket(buf, seq_num); + } else if (mFPPratioNumerator > 1) { + // 2/1, 4/1 peer FPP is lower, (local/peer)/1 + assemblePacket(buf, seq_num); } else { - seq_num %= mModSeqNumPeer; - if (mFPPratioNumerator > 1) { // 2/1, 4/1 peer FPP is lower, , (local/peer)/1 - int tmp = (seq_num % mFPPratioNumerator) * mBytesPeerPacket; - memcpy(&mAssembledPacket[tmp], buf, mBytesPeerPacket); - if ((seq_num % mFPPratioNumerator) == mModCycle) { - if (mAssemblyCnt == mModCycle) - pushPacket(mAssembledPacket, seq_num / mFPPratioNumerator); - // else - // qDebug() << "incomplete due to lost packet"; - mAssemblyCnt = 0; - } else - mAssemblyCnt++; - } else if (mFPPratioDenominator - > 1) { // 1/2, 1/4 peer FPP is higher, 1/(peer/local) - seq_num *= mFPPratioDenominator; - for (int i = 0; i < mFPPratioDenominator; i++) { - int tmp = i * mBytes; - memcpy(mAssembledPacket, &buf[tmp], mBytes); - pushPacket(mAssembledPacket, seq_num); - seq_num++; - } + // 1/2, 1/4 peer FPP is higher, 1/(peer/local) + seq_num *= mFPPratioDenominator; + for (int i = 0; i < mFPPratioDenominator; i++) { + memcpy(mAssembledPacket, buf, mBytes); + pushPacket(mAssembledPacket, seq_num); + buf += mBytes; + seq_num++; } } pushStat->tick(); - double adjustAuto = - pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec, mPeerFPPdurMsec); - // qDebug() << adjustAuto; - if (mAuto && (pushStat->lastTime > AutoInitDur)) - mMsecTolerance = adjustAuto; + if (mAuto && (pushStat->lastTime > AutoInitDur)) { + // use max to accomodate for bad clocks in audio interfaces that + // cause a wide range of callback intervals (like realtek at 11ms) + mMsecTolerance = std::max( + pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec, mPeerFPPdurMsec), + pullStat->calcAuto(mAutoHeadroom, mFPPdurMsec, mPeerFPPdurMsec)); + } } }; @@ -384,54 +359,82 @@ void Regulator::pushPacket(const int8_t* buf, int seq_num) { if (m_b_BroadcastQueueLength) m_b_BroadcastRingBuffer->insertSlotNonBlocking(buf, mBytes, 0, seq_num); - seq_num %= mModSeqNum; + seq_num %= mNumSlots; // if (seq_num==0) return; // impose regular loss mIncomingTiming[seq_num] = mMsecTolerance + (double)mIncomingTimer.nsecsElapsed() / 1000000.0; - if (seq_num != -1) - memcpy(mSlots[seq_num % mNumSlots], buf, mBytes); + memcpy(mSlots[seq_num], buf, mBytes); + mLastSeqNumIn.store(seq_num, std::memory_order_release); +}; + +//******************************************************************************* +void Regulator::assemblePacket(const int8_t* buf, int peer_seq_num) +{ + // copy packet fragment into slot + int seq_num = (peer_seq_num / mFPPratioNumerator) % mNumSlots; + int pkt_pos = (peer_seq_num % mFPPratioNumerator); + memcpy(&(mSlots[seq_num][pkt_pos * mBytesPeerPacket]), buf, mBytesPeerPacket); + + // check if done assembling yet + if (++mAssemblyCounts[seq_num] < mFPPratioNumerator) + return; + + // complete it + if (m_b_BroadcastQueueLength) + m_b_BroadcastRingBuffer->insertSlotNonBlocking(mSlots[seq_num], mBytes, 0, + seq_num); + mIncomingTiming[seq_num] = + mMsecTolerance + (double)mIncomingTimer.nsecsElapsed() / 1000000.0; mLastSeqNumIn.store(seq_num, std::memory_order_release); }; //******************************************************************************* void Regulator::pullPacket() { - int lastSeqNumIn = mLastSeqNumIn.load(std::memory_order_acquire); - mSkip = 0; + const double now = (double)mIncomingTimer.nsecsElapsed() / 1000000.0; + const int lastSeqNumIn = mLastSeqNumIn.load(std::memory_order_acquire); + mSkip = 0; + if ((lastSeqNumIn == -1) || (!mFPPratioIsSet)) { goto ZERO_OUTPUT; + } else if (lastSeqNumIn == mLastSeqNumOut) { + goto UNDERRUN; } else { - mLastSeqNumOut++; - mLastSeqNumOut %= mModSeqNum; - double now = (double)mIncomingTimer.nsecsElapsed() / 1000000.0; - for (int i = mLostWindow; i >= 0; i--) { + // calculate how many new packets we want to look at to + // find the next packet to pull + int new_pkts = lastSeqNumIn - mLastSeqNumOut; + if (new_pkts < 0) + new_pkts += mNumSlots; + + // iterate through each new packet + for (int i = new_pkts - 1; i >= 0; i--) { int next = lastSeqNumIn - i; if (next < 0) - next += mModSeqNum; - if (mIncomingTiming[next] < mIncomingTiming[mLastSeqNumOut]) - continue; - mSkip = next - mLastSeqNumOut; - if (mSkip < 0) - mSkip += mModSeqNum; + next += mNumSlots; + if (mFPPratioNumerator) { + // time for assembly has passed; reset for next time + mAssemblyCounts[next] = 0; + } + if (mLastSeqNumOut != -1) { + // account for missing packets + if (mIncomingTiming[next] < mIncomingTiming[mLastSeqNumOut]) + continue; + // count how many we have skipped + mSkip = next - mLastSeqNumOut - 1; + if (mSkip < 0) + mSkip += mNumSlots; + } + // set next as the best candidate mLastSeqNumOut = next; - if (mIncomingTiming[next] > now) { - memcpy(mXfrBuffer, mSlots[mLastSeqNumOut % mNumSlots], mBytes); + // if next timestamp < now, it is too old based upon tolerance + if (mIncomingTiming[mLastSeqNumOut] >= now) { + // next is the best candidate + memcpy(mXfrBuffer, mSlots[mLastSeqNumOut], mBytes); goto PACKETOK; } } - // make this a global value? -- same threshold as - // UdpDataProtocol::printUdpWaitedTooLong - double wait_time = 30; // msec - if ((mLastSeqNumOut == lastSeqNumIn) - && ((now - mIncomingTiming[mLastSeqNumOut]) > wait_time)) { - // std::cout << (mIncomingTiming[mLastSeqNumOut] - now) - // << "lastSeqNumIn: " << lastSeqNumIn << - // "\tmLastSeqNumOut: " << mLastSeqNumOut << std::endl; - goto ZERO_OUTPUT; - } // "good underrun", not a stuck client - // std::cout << "within window -- lastSeqNumIn: " << - // lastSeqNumIn << - // "\tmLastSeqNumOut: " << mLastSeqNumOut << std::endl; + + // no viable candidate goto UNDERRUN; } @@ -441,14 +444,17 @@ PACKETOK : { pullStat->plcOverruns += mSkip; } else processPacket(false); - pullStat->tick(); goto OUTPUT; } UNDERRUN : { - processPacket(true); pullStat->plcUnderruns++; // count late - pullStat->tick(); + if ((mLastSeqNumOut == lastSeqNumIn) + && ((now - mIncomingTiming[mLastSeqNumOut]) > MaxWaitTime)) { + goto ZERO_OUTPUT; + } + // "good underrun", not a stuck client + processPacket(true); goto OUTPUT; } @@ -456,6 +462,7 @@ ZERO_OUTPUT: memcpy(mXfrBuffer, mZeros, mBytes); OUTPUT: + pullStat->tick(); return; }; @@ -463,9 +470,6 @@ OUTPUT: void Regulator::processPacket(bool glitch) { double tmp = 0.0; - if ((glitch) && (mFPPratioDenominator > 1)) { - glitch = !(mLastSeqNumOut % mFPPratioDenominator); - } if (glitch) tmp = (double)mIncomingTimer.nsecsElapsed(); for (int ch = 0; ch < mNumChannels; ch++) @@ -725,20 +729,23 @@ StdDev::StdDev(int id, QElapsedTimer* timer, int w) : mId(id), mTimer(timer), wi lastMax = 0.0; longTermMax = 0.0; longTermMaxAcc = 0.0; + longTermMean = 0.0; lastTime = 0.0; lastPLCdspElapsed = 0.0; + lastPlcOverruns = 0; + lastPlcUnderruns = 0; + plcOverruns = 0; + plcUnderruns = 0; data.resize(w, 0.0); } void StdDev::reset() { - ctr = 0; - plcOverruns = 0; - plcUnderruns = 0; - mean = 0.0; - acc = 0.0; - min = 999999.0; - max = -999999.0; + ctr = 0; + mean = 0.0; + acc = 0.0; + min = 999999.0; + max = -999999.0; }; double StdDev::calcAuto(double autoHeadroom, double localFPPdur, double peerFPPdur) @@ -748,6 +755,8 @@ double StdDev::calcAuto(double autoHeadroom, double localFPPdur, double peerFPPd if ((longTermStdDev == 0.0) || (longTermMax == 0.0)) return AutoMax; double tmp = longTermStdDev + ((longTermMax > AutoMax) ? AutoMax : longTermMax); + if (tmp > AutoMax) + tmp = AutoMax; if (tmp < localFPPdur) tmp = localFPPdur; if (tmp < peerFPPdur) @@ -761,6 +770,10 @@ void StdDev::tick() double now = (double)mTimer->nsecsElapsed() / 1000000.0; double msElapsed = now - lastTime; lastTime = now; + // discard measurements that exceed the max wait time + // this prevents temporary outages from skewing jitter metrics + if (msElapsed > MaxWaitTime) + return; if (ctr != window) { data[ctr] = msElapsed; if (msElapsed < min) @@ -769,7 +782,16 @@ void StdDev::tick() max = msElapsed; acc += msElapsed; ctr++; + /* + // for debugging startup issues -- you'll see a bunch of pushes + // UDPDataProtocol all at once, which I imagine were queued up + // in the kernel's stack + if (gVerboseFlag && longTermCnt == 0) { + std::cout << setw(10) << msElapsed << " " << mId << endl; + } + */ } else { + // calculate mean and standard deviation mean = (double)acc / (double)window; double var = 0.0; for (int i = 0; i < window; i++) { @@ -778,33 +800,50 @@ void StdDev::tick() } var /= (double)window; double stdDevTmp = sqrt(var); - if (longTermCnt) { + + if (longTermCnt <= 1) { + if (longTermCnt == 0 && gVerboseFlag) { + cout << "printing directly from Regulator->stdDev->tick:\n (mean / min / " + "max / " + "stdDev / longTermMean / longTermMax / longTermStdDev) \n"; + } + // ignore first stats because they will be really unreliable + longTermMax = max; + longTermMaxAcc = max; + longTermMean = mean; + longTermStdDev = stdDevTmp; + longTermStdDevAcc = stdDevTmp; + } else { longTermStdDevAcc += stdDevTmp; - longTermStdDev = longTermStdDevAcc / (double)longTermCnt; longTermMaxAcc += max; - longTermMax = longTermMaxAcc / (double)longTermCnt; - if (gVerboseFlag) - cout << setw(10) << mean << setw(10) << lastMin << setw(10) << max - << setw(10) << stdDevTmp << setw(10) << longTermStdDev << " " << mId - << endl; - } else if (gVerboseFlag) - cout << "printing directly from Regulator->stdDev->tick:\n (mean / min / " - "max / " - "stdDev / longTermStdDev) \n"; + longTermStdDev = longTermStdDevAcc / (double)longTermCnt; + longTermMax = longTermMaxAcc / (double)longTermCnt; + longTermMean = longTermMean / (double)longTermCnt; + } + + if (gVerboseFlag) { + cout << setw(10) << mean << setw(10) << min << setw(10) << max << setw(10) + << stdDevTmp << setw(10) << longTermMean << setw(10) << longTermMax + << setw(10) << longTermStdDev << " " << mId << endl; + } longTermCnt++; - lastMean = mean; - lastMin = min; - lastMax = max; - lastPlcOverruns = plcOverruns; - lastPlcUnderruns = plcUnderruns; - lastStdDev = stdDevTmp; + lastMean = mean; + lastMin = min; + lastMax = max; + lastStdDev = stdDevTmp; reset(); } } void Regulator::readSlotNonBlocking(int8_t* ptrToReadSlot) { + if (!mFPPratioIsSet) { + // audio callback before receiving first packet from peer + // nothing is initialized yet, so just return silence + memcpy(ptrToReadSlot, mZeros, mBytes); + return; + } if (mUseWorkerThread) { // use separate worker thread for PLC mRegulatorWorkerPtr->pop(ptrToReadSlot); @@ -835,7 +874,10 @@ bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset) } // hijack of struct IOStat { - stat->underruns = pullStat->lastPlcUnderruns + pullStat->lastPlcOverruns; + stat->underruns = (pullStat->plcUnderruns - pullStat->lastPlcUnderruns) + + (pullStat->plcOverruns - pullStat->lastPlcOverruns); + pullStat->lastPlcUnderruns = pullStat->plcUnderruns; + pullStat->lastPlcOverruns = pullStat->plcOverruns; #define FLOATFACTOR 1000.0 stat->overflows = FLOATFACTOR * pushStat->longTermStdDev; stat->skew = FLOATFACTOR * pushStat->lastMean; diff --git a/src/Regulator.h b/src/Regulator.h index 97c9e67..938b032 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -113,6 +113,7 @@ class StdDev double longTermStdDevAcc; double longTermMax; double longTermMaxAcc; + double longTermMean; private: void reset(); @@ -185,6 +186,7 @@ class Regulator : public RingBuffer private: void shimFPP(const int8_t* buf, int len, int seq_num); void pushPacket(const int8_t* buf, int seq_num); + void assemblePacket(const int8_t* buf, int peer_seq_num); void pullPacket(); void setFPPratio(); bool mFPPratioIsSet; @@ -221,15 +223,11 @@ class Regulator : public RingBuffer int mLastSeqNumOut; std::vector mPhasor; std::vector mIncomingTiming; - int mModSeqNum; - int mLostWindow; + std::vector mAssemblyCounts; int mSkip; int mFPPratioNumerator; int mFPPratioDenominator; - int mAssemblyCnt; - int mModCycle; bool mAuto; - int mModSeqNumPeer; double mAutoHeadroom; double mFPPdurMsec; double mPeerFPPdurMsec; @@ -262,6 +260,7 @@ class RegulatorWorker : public QObject , mPacketQueue(rPtr->getPacketSize()) , mPacketQueueTarget(1) , mLastUnderrun(0) + , mSkipQueueUpdate(true) , mUnderrun(false) , mStarted(false) { @@ -312,12 +311,20 @@ class RegulatorWorker : public QObject { if (mUnderrun.load(std::memory_order_relaxed)) { if (mStarted) { - // allow up to 1 underrun per second before adjusting target double now = (double)mRegulatorPtr->mIncomingTimer.nsecsElapsed() / 1000000.0; - if (mLastUnderrun != 0 && now - mLastUnderrun < 1000.0) - updateQueueTarget(); - mLastUnderrun = now; + // only adjust target at most once per 1.0 seconds + if (now - mLastUnderrun >= 1000.0) { + if (mSkipQueueUpdate) { + // require consecutive underruns periods to bump target + mSkipQueueUpdate = false; + } else if (now - mLastUnderrun < 2000.0) { + // previous period had underruns + updateQueueTarget(); + mSkipQueueUpdate = true; + } // else, skip this period but not the next one + mLastUnderrun = now; + } mUnderrun.store(false, std::memory_order_relaxed); } else { mStarted = true; @@ -334,24 +341,19 @@ class RegulatorWorker : public QObject private: void updateQueueTarget() { - // cap queue size at 4x the time it takes to run a prediction - double samples = - (mRegulatorPtr->getLastDspElapsed() * 4 * mRegulatorPtr->getSampleRate()) - / 1000; - std::size_t maxPackets = (samples / mRegulatorPtr->getBufferSizeInSamples()) + 1; - if (maxPackets > mPacketQueue.capacity() / 2) - maxPackets = mPacketQueue.capacity() / 2; - if (mPacketQueueTarget < maxPackets) { - // adjust queue target - ++mPacketQueueTarget; - std::cout << "PLC worker queue: adjusting target=" << mPacketQueueTarget - << " (max=" << maxPackets - << ", lastDspElapsed=" << mRegulatorPtr->getLastDspElapsed() << ")" - << std::endl; - if (mPacketQueueTarget == maxPackets) { - emit signalMaxQueueSize(); - std::cout << "PLC worker queue: reached MAX target!" << std::endl; - } + // sanity check + const std::size_t maxPackets = mPacketQueue.capacity() / 2; + if (mPacketQueueTarget > maxPackets) + return; + // adjust queue target + ++mPacketQueueTarget; + std::cout << "PLC worker queue: adjusting target=" << mPacketQueueTarget + << " (max=" << maxPackets + << ", lastDspElapsed=" << mRegulatorPtr->getLastDspElapsed() << ")" + << std::endl; + if (mPacketQueueTarget == maxPackets) { + emit signalMaxQueueSize(); + std::cout << "PLC worker queue: reached MAX target!" << std::endl; } } @@ -367,6 +369,9 @@ class RegulatorWorker : public QObject /// time of last underrun, in milliseconds double mLastUnderrun; + /// true if the next packet queue update should be skipped + bool mSkipQueueUpdate; + /// last value of packet queue underruns std::atomic mUnderrun; diff --git a/src/Settings.cpp b/src/Settings.cpp index ab35547..28ffb13 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -99,6 +99,7 @@ enum JTLongOptIDS { OPT_AUDIOINPUTDEVICE, OPT_AUDIOOUTPUTDEVICE, OPT_GUI, + OPT_CLASSIC_GUI, OPT_DEEPLINK }; @@ -208,7 +209,9 @@ void Settings::parseInput(int argc, char** argv) {"examine-audio-delay", required_argument, NULL, 'x'}, // test mode - measure audio round-trip latency statistics {"gui", no_argument, NULL, OPT_GUI}, // Force GUI mode - {"deeplink", optional_argument, NULL, OPT_DEEPLINK}, // VirtualStudio Deeplink + {"classic-gui", no_argument, NULL, OPT_CLASSIC_GUI}, // Force Classic Mode GUI + {"deeplink", optional_argument, NULL, + OPT_DEEPLINK}, // Deeplink URL (should be in the form jacktrip://...) {NULL, 0, NULL, 0}}; // Parse Command Line Arguments @@ -342,9 +345,10 @@ void Settings::parseInput(int argc, char** argv) case 'q': //------------------------------------------------------- if (0 == strncmp(optarg, "auto", 4)) { - mBufferQueueLength = -atoi(optarg + 4); - if (0 == mBufferQueueLength) { + if (optarg[4] == 0) { mBufferQueueLength = -500; + } else { + mBufferQueueLength = -atoi(optarg + 4); } } else if (atoi(optarg) <= 0) { printUsage(); @@ -678,14 +682,18 @@ void Settings::parseInput(int argc, char** argv) mAudioTester->setPrintIntervalSec(atof(optarg)); break; } - // The following two options need to be handled earlier, so are all parsed in + // The following option needs to be handled earlier, so are all parsed in // main. Included here so that we don't get an unrecognized option error. case OPT_GUI: break; + case OPT_CLASSIC_GUI: + mGuiForceClassicMode = true; + break; case OPT_DEEPLINK: if (optarg == NULL && optind < argc && argv[optind][0] != '-') { optarg = argv[optind++]; } + mDeeplink = optarg; break; case ':': { printUsage(); @@ -710,10 +718,16 @@ void Settings::parseInput(int argc, char** argv) } } + // allow deeplink in command line without option + if (optind < argc && strncmp(argv[optind], "jacktrip://", 11) == 0) { + mDeeplink = argv[optind]; + optind++; + } + // Warn user if undefined options where entered //---------------------------------------------------------------------------- if (optind < argc) { - if (strcmp(argv[optind], "help") != 0) { + if (strncmp(argv[optind], "help", 4) != 0) { cout << gPrintSeparator << endl; cout << "*** Unexpected command-line argument(s): "; for (; optind < argc; optind++) { @@ -949,7 +963,9 @@ void Settings::printUsage() cout << " --password The password to use when connecting as a hub client (if not supplied here, this is read from standard input)" << endl; cout << endl; cout << "ARGUMENTS FOR THE GUI:" << endl; - cout << " --gui Force JackTrip to run with the GUI. If not using VirtualStudio mode, command line switches in the required arguments, optional arguments (except -l, -j, -L, --appendthreadid), audio patching, and authentication sections will be honoured, and default settings will be used where arguments aren't supplied. Options from other sections will be ignored (and the last used settings will be loaded), except for -V, and the --version and --help switches which will override this." << endl; + cout << " --gui Force JackTrip to run with the GUI. If not using VirtualStudio mode, command line switches in the required arguments, optional arguments (except -l, -j, -L, --appendthreadid), audio patching, and authentication sections will be honoured, and default settings will be used where arguments aren't supplied. Options from other sections will be ignored (and the last used settings will be loaded), except for -V, and the --version and --help switches which will override this." << endl; + cout << " --classic-gui Force JackTrip to run with the Classic Mode GUI." << endl; + cout << " --deeplink Handle a deeplink URL in the format jacktrip://join/ by connecting as a hub client" << endl; cout << endl; cout << "HELP ARGUMENTS: " << endl; cout << " -v, --version Prints Version Number" << endl; diff --git a/src/Settings.h b/src/Settings.h index 6b556f8..a189a8e 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -84,6 +84,7 @@ class Settings : public QObject bool getLoopBack() { return mLoopBack; } bool isHubServer() { return mJackTripMode == JackTrip::SERVERPINGSERVER; } bool guiIgnoresArguments() { return mGuiIgnoresArguments; } + bool guiForceClassicMode() { return mGuiForceClassicMode; } bool isModeSet() { return mModeSet; } JackTrip::jacktripModeT getJackTripMode() { return mJackTripMode; } @@ -106,6 +107,7 @@ class Settings : public QObject bool getConnectDefaultAudioPorts() { return mConnectDefaultAudioPorts; } int getBufferStrategy() { return mBufferStrategy; } int getBroadCastQueue() { return mBroadcastQueue; } + int getIOStatTimeout() { return mIOStatTimeout; } bool getUseRtUdpPriority() { return mUseRtUdpPriority; } unsigned int getHubConnectionMode() { return mHubConnectionMode; } bool getPatchServerAudio() { return mPatchServerAudio; } @@ -116,6 +118,7 @@ class Settings : public QObject QString getCredsFile() { return mCredsFile; } QString getUsername() { return mUsername; } QString getPassword() { return mPassword; } + const QString& getDeeplink() const { return mDeeplink; } private: void disableEcho(bool disabled); @@ -123,6 +126,7 @@ class Settings : public QObject bool mGuiEnabled = false; bool mGuiIgnoresArguments = false; + bool mGuiForceClassicMode = false; JackTrip::jacktripModeT mJackTripMode = JackTrip::SERVER; ///< JackTrip::jacktripModeT @@ -189,6 +193,7 @@ class Settings : public QObject QString mCredsFile; QString mUsername; QString mPassword; + QString mDeeplink; QSharedPointer mAudioTester; }; diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index fecee8e..73f975a 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -91,8 +91,7 @@ UdpDataProtocol::UdpDataProtocol(JackTrip* jacktrip, const runModeT runmode, , mControlPacketSize(63) , mStopSignalSent(false) { - mStopped = false; - mIPv6 = false; + mIPv6 = false; std::memset(&mPeerAddr, 0, sizeof(mPeerAddr)); std::memset(&mPeerAddr6, 0, sizeof(mPeerAddr6)); mPeerAddr.sin_port = htons(mPeerPort); @@ -551,6 +550,8 @@ void UdpDataProtocol::run() // // clang-format off + threadHasStarted(); + switch (mRunMode) { case RECEIVER: { // Connect signals and slots for packets arriving too late notifications diff --git a/src/gui/ChangeDevices.qml b/src/gui/ChangeDevices.qml index 76a2ae7..47f97d5 100644 --- a/src/gui/ChangeDevices.qml +++ b/src/gui/ChangeDevices.qml @@ -61,6 +61,11 @@ Rectangle { return idx; } + MouseArea { + anchors.fill: parent + propagateComposedEvents: false + } + Rectangle { width: parent.width; height: 360 anchors.verticalCenter: parent.verticalCenter diff --git a/src/gui/CreateStudio.qml b/src/gui/CreateStudio.qml new file mode 100644 index 0000000..4228f51 --- /dev/null +++ b/src/gui/CreateStudio.qml @@ -0,0 +1,124 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtWebEngine +import org.jacktrip.jacktrip 1.0 + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontMedium: 12 + property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + Loader { + id: webLoader + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: footer.top + property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" + sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull + } + + Component { + id: createStudioNull + Rectangle { + anchors.fill: parent + color: backgroundColour + } + } + + Component { + id: createStudioWeb + WebEngineView { + id: webEngineView + anchors.fill: parent + settings.javascriptCanAccessClipboard: true + settings.javascriptCanPaste: true + settings.screenCaptureEnabled: true + profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` + url: `https://${virtualstudio.apiHost}/qt/create?accessToken=${accessToken}` + + onContextMenuRequested: function(request) { + // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop + request.accepted = true; + } + + onNewWindowRequested: function(request) { + Qt.openUrlExternally(request.requestedUrl); + } + + onFeaturePermissionRequested: function(securityOrigin, feature) { + webEngineView.grantFeaturePermission(securityOrigin, feature, true); + } + + onRenderProcessTerminated: function(terminationStatus, exitCode) { + var status = ""; + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)"; + break; + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)"; + break; + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)"; + break; + case WebEngineView.KilledTerminationStatus: + status = "(killed)"; + break; + } + console.log("Render process exited with code " + exitCode + " " + status); + } + } + } + + Rectangle { + id: footer + anchors.bottom: parent.bottom + width: parent.width + height: 48 + color: backgroundColour + + RowLayout { + id: layout + anchors.fill: parent + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + Button { + id: backButton + anchors.centerIn: parent + width: 180 * virtualstudio.uiScale + height: 36 * virtualstudio.uiScale + background: Rectangle { + radius: 8 * virtualstudio.uiScale + color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) + } + onClicked: virtualstudio.windowState = "browse" + + Text { + text: "Back to Studios" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + } + + Rectangle { + id: backgroundBorder + width: parent.width + height: 1 + y: parent.height - footer.height + color: buttonStroke + } + } +} diff --git a/src/gui/DeviceControlsGroup.qml b/src/gui/DeviceControlsGroup.qml index d0bb216..dc5f716 100644 --- a/src/gui/DeviceControlsGroup.qml +++ b/src/gui/DeviceControlsGroup.qml @@ -37,7 +37,7 @@ Rectangle { id: backButton anchors.centerIn: parent width: 180 * virtualstudio.uiScale - height: 40 * virtualstudio.uiScale + height: 36 * virtualstudio.uiScale background: Rectangle { radius: 8 * virtualstudio.uiScale color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index 8e2b9f5..574dbdf 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -18,6 +18,7 @@ Meter.qml MeterBars.qml Connected.qml + CreateStudio.qml Failed.qml Setup.qml SectionHeading.qml diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp index a1fd472..9d0ed18 100644 --- a/src/gui/virtualstudio.cpp +++ b/src/gui/virtualstudio.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -87,17 +88,20 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) QSvgGenerator svgImageHack; // use a singleton QNetworkAccessManager - m_networkAccessManager.reset(new QNetworkAccessManager); + // WARNING: using a raw pointer and intentionally leaking this because + // it crashes at shutdown if you try to destruct it directly or try + // calling QObject::deleteLater() + m_networkAccessManagerPtr = new QNetworkAccessManager; // instantiate API - m_api.reset(new VsApi(m_networkAccessManager.data())); + m_api.reset(new VsApi(m_networkAccessManagerPtr)); m_api->setApiHost(PROD_API_HOST); if (m_testMode) { m_api->setApiHost(TEST_API_HOST); } // instantiate auth - m_auth.reset(new VsAuth(m_networkAccessManager.data(), m_api.data())); + m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data())); connect(m_auth.data(), &VsAuth::authSucceeded, this, &VirtualStudio::slotAuthSucceeded); connect(m_auth.data(), &VsAuth::refreshTokenFailed, this, [=]() { @@ -132,8 +136,10 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_networkOutageTimer.setSingleShot(true); m_networkOutageTimer.setInterval(5000); m_networkOutageTimer.callOnTimeout([&]() { - m_networkOutage = false; - emit updatedNetworkOutage(m_networkOutage); + if (m_devicePtr.isNull()) + return; + m_devicePtr->setNetworkOutage(false); + emit updatedNetworkOutage(false); }); if ((m_uiMode == QJackTrip::UNSET && vsFtux()) @@ -195,6 +201,11 @@ void VirtualStudio::setStandardWindow(QSharedPointer window) m_standardWindow = window; } +void VirtualStudio::setCLISettings(QSharedPointer settings) +{ + m_cliSettings = settings; +} + void VirtualStudio::show() { if (m_checkSsl) { @@ -214,13 +225,40 @@ void VirtualStudio::show() } m_checkSsl = false; } - m_view.show(); + + while (m_view.status() == QQuickView::Loading) { + // I don't think there is any need to load network data, but just in case + // See https://doc.qt.io/qt-6/qquickview.html#Status-enum + qDebug() << "JackTrip is still loading the QML view"; + QThread::sleep(1); + } + + if (m_view.status() != QQuickView::Ready) { + QMessageBox msgBox; + msgBox.setText( + "JackTrip detected that some modules required for the " + "Virtual Studio mode are missing on your system. " + "Click \"OK\" to proceed to classic mode.\n\n" + "Details: JackTrip failed to load the QML view. " + "This is likely caused by missing QML plugins. " + "Please consult help.jacktrip.org for possible solutions."); + msgBox.setWindowTitle(QStringLiteral("JackTrip Is Missing QML Modules")); + connect(&msgBox, &QMessageBox::finished, this, &VirtualStudio::toStandard, + Qt::QueuedConnection); + msgBox.exec(); + return; + } + + raiseToTop(); } void VirtualStudio::raiseToTop() { + if (m_view.status() != QQuickView::Ready) + return; m_view.show(); // Restore from systray - m_view.requestActivate(); // Raise to top + m_view.raise(); // raise to top + m_view.requestActivate(); // focus on window } int VirtualStudio::webChannelPort() @@ -271,7 +309,7 @@ void VirtualStudio::setConnectedErrorMsg(const QString& msg) bool VirtualStudio::networkOutage() { - return m_networkOutage; + return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage(); } QJsonObject VirtualStudio::regions() @@ -408,8 +446,28 @@ bool VirtualStudio::isExiting() void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString message) { QJsonObject feedback; + + QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName()); +#ifdef RT_AUDIO + QString inputDevice = + QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString()); + if (!inputDevice.isEmpty()) { + sysInfo.append(QString(",input=%1").arg(inputDevice)); + } + QString outputDevice = + QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString()); + if (!outputDevice.isEmpty()) { + sysInfo.append(QString(",output=%1").arg(outputDevice)); + } +#endif + sysInfo.append("]"); + feedback.insert(QStringLiteral("rating"), rating); - feedback.insert(QStringLiteral("message"), message); + if (message.isEmpty()) { + feedback.insert(QStringLiteral("message"), sysInfo); + } else { + feedback.insert(QStringLiteral("message"), message + " " + sysInfo); + } QJsonDocument data = QJsonDocument(feedback); m_api->submitServerFeedback(serverId, data.toJson()); @@ -737,6 +795,7 @@ void VirtualStudio::loadSettings() void VirtualStudio::saveSettings() { QSettings settings; + settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel); settings.beginGroup(QStringLiteral("VirtualStudio")); settings.setValue(QStringLiteral("UiScale"), m_uiScale); settings.setValue(QStringLiteral("DarkMode"), m_darkMode); @@ -793,7 +852,6 @@ void VirtualStudio::completeConnection() bool useRtAudio = m_audioConfigPtr->getUseRtAudio(); std::string input = ""; std::string output = ""; - int buffer_strategy = m_audioConfigPtr->getBufferStrategy(); int buffer_size = 0; int inputMixMode = -1; int baseInputChannel = 0; @@ -816,6 +874,27 @@ void VirtualStudio::completeConnection() numOutputChannels = m_audioConfigPtr->getNumOutputChannels(); } #endif + + // increment buffer_strategy by 1 for array-index mapping + int buffer_strategy = m_audioConfigPtr->getBufferStrategy() + 1; + // adjust buffer_strategy for PLC "auto" mode menu item + if (buffer_strategy == 3) { + if (useRtAudio) { + // if same device for input and output, + // run PLC without worker (4) + if (input == output) + buffer_strategy = 4; + // else run PLC with worker (3) + // to reduce crackles + } else { + // run PLC without worker (4) + buffer_strategy = 4; + } + } else if (buffer_strategy == 5) { + buffer_strategy = 3; // run PLC with worker (3) + } + + // create a new JackTrip instance JackTrip* jackTrip = m_devicePtr->initJackTrip( useRtAudio, input, output, baseInputChannel, numInputChannels, baseOutputChannel, numOutputChannels, inputMixMode, buffer_size, @@ -824,6 +903,7 @@ void VirtualStudio::completeConnection() processError("Could not bind port"); return; } + jackTrip->setIOStatTimeout(m_cliSettings->getIOStatTimeout()); // this passes ownership to JackTrip jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip)); @@ -990,8 +1070,7 @@ void VirtualStudio::launchVideo(const QString& studioId) void VirtualStudio::createStudio() { - QUrl url = QUrl(QStringLiteral("https://%1/studios/create").arg(m_api->getApiHost())); - QDesktopServices::openUrl(url); + setWindowState(QStringLiteral("create_studio")); } void VirtualStudio::editProfile() @@ -1035,7 +1114,6 @@ void VirtualStudio::handleDeeplinkRequest(const QUrl& link) // Note that this doesn't change the startup preference if (m_uiMode != QJackTrip::VIRTUAL_STUDIO) { m_standardWindow->hide(); - m_view.show(); if (m_windowState == "start") { setWindowState(QStringLiteral("login")); } @@ -1058,6 +1136,18 @@ void VirtualStudio::handleDeeplinkRequest(const QUrl& link) return; } + // special case if on create_studio screen + if (m_windowState == "create_studio") { + refreshStudios(0, true); + if (showDeviceSetup()) { + setWindowState("setup"); + m_audioConfigPtr->startAudio(); + } else { + setWindowState("connected"); + } + return; + } + // special case if on browsing screen if (m_windowState == "browse") { setWindowState("connected"); @@ -1100,6 +1190,9 @@ void VirtualStudio::exit() void VirtualStudio::slotAuthSucceeded() { + // Make sure window is on top (instead of browser, during first auth) + raiseToTop(); + // Determine which API host to use m_apiHost = PROD_API_HOST; if (m_testMode) { @@ -1319,9 +1412,11 @@ void VirtualStudio::updatedStats(const QJsonObject& stats) void VirtualStudio::udpWaitingTooLong() { + if (m_devicePtr.isNull()) + return; m_networkOutageTimer.start(); - m_networkOutage = true; - emit updatedNetworkOutage(m_networkOutage); + m_devicePtr->setNetworkOutage(true); + emit updatedNetworkOutage(true); } void VirtualStudio::sendHeartbeat() @@ -1625,4 +1720,6 @@ void VirtualStudio::detectedFeedbackLoop() VirtualStudio::~VirtualStudio() { QDesktopServices::unsetUrlHandler("jacktrip"); + // stop the audio worker thread before destructing other things + m_audioConfigPtr.reset(); } diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h index 076fa3c..036b808 100644 --- a/src/gui/virtualstudio.h +++ b/src/gui/virtualstudio.h @@ -52,6 +52,7 @@ #include #include +#include "../Settings.h" #include "qjacktrip.h" #include "vsConstants.h" #include "vsQuickView.h" @@ -127,6 +128,7 @@ class VirtualStudio : public QObject ~VirtualStudio() override; void setStandardWindow(QSharedPointer window); + void setCLISettings(QSharedPointer settings); void show(); void raiseToTop(); @@ -275,8 +277,9 @@ class VirtualStudio : public QObject VsQuickView m_view; VsServerInfo m_currentStudio; + QNetworkAccessManager* m_networkAccessManagerPtr; QSharedPointer m_standardWindow; - QScopedPointer m_networkAccessManager; + QSharedPointer m_cliSettings; QSharedPointer m_auth; QSharedPointer m_api; QScopedPointer m_devicePtr; @@ -319,7 +322,6 @@ class VirtualStudio : public QObject bool m_collapseDeviceControls = false; bool m_testMode = false; bool m_authenticated = false; - bool m_networkOutage = false; float m_fontScale = 1; float m_uiScale = 1; uint32_t m_webChannelPort = 1; diff --git a/src/gui/vs.qml b/src/gui/vs.qml index ee4913e..4607c15 100644 --- a/src/gui/vs.qml +++ b/src/gui/vs.qml @@ -20,6 +20,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -34,6 +35,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -48,6 +50,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -62,6 +65,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -76,6 +80,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: 0 } PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -90,6 +95,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: 0 } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -104,11 +110,27 @@ Rectangle { PropertyChanges { target: setupScreen; x: window.width } PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: 0 } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: window.width } }, + State { + name: "create_studio" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: 0 } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + State { name: "connected" PropertyChanges { target: loginScreen; x: -loginScreen.width } @@ -118,6 +140,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: 0 } PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } PropertyChanges { target: connectedScreen; x: 0 } PropertyChanges { target: changeDevicesScreen; x: window.width } PropertyChanges { target: failedScreen; x: window.width } @@ -132,6 +155,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: 0 } PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } PropertyChanges { target: connectedScreen; x: 0 } PropertyChanges { target: changeDevicesScreen; x: 0 } PropertyChanges { target: failedScreen; x: window.width } @@ -146,6 +170,7 @@ Rectangle { PropertyChanges { target: setupScreen; x: -setupScreen.width } PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } PropertyChanges { target: changeDevicesScreen; x: 2*window.width } PropertyChanges { target: failedScreen; x: 0 } @@ -192,6 +217,10 @@ Rectangle { id: changeDevicesScreen } + CreateStudio { + id: createStudioScreen + } + Failed { id: failedScreen } @@ -211,6 +240,8 @@ Rectangle { browseScreen.x = 0 } else if (virtualstudio.windowState === "settings") { settingsScreen.x = 0 + } else if (virtualstudio.windowState === "create_studio") { + createStudioScreen.x = 0 } else if (virtualstudio.windowState === "connected") { connectedScreen.x = 0 } else if (virtualstudio.windowState === "change_devices") { @@ -235,6 +266,8 @@ Rectangle { browseScreen.x = 0 } else if (virtualstudio.windowState === "settings") { settingsScreen.x = 0 + } else if (virtualstudio.windowState === "create_studio") { + createStudioScreen.x = 0 } else if (virtualstudio.windowState === "connected") { connectedScreen.x = 0 } else if (virtualstudio.windowState === "change_devices") { diff --git a/src/gui/vsAudio.cpp b/src/gui/vsAudio.cpp index 4227ac4..96b584e 100644 --- a/src/gui/vsAudio.cpp +++ b/src/gui/vsAudio.cpp @@ -154,10 +154,10 @@ VsAudio::VsAudio(QObject* parent) }); // move audio worker to its own thread - m_workerThread.reset(new QThread); - m_workerThread->setObjectName("VsAudioWorker"); - m_workerThread->start(); - m_audioWorkerPtr->moveToThread(m_workerThread.get()); + m_workerThreadPtr = new QThread; + m_workerThreadPtr->setObjectName("VsAudioWorker"); + m_workerThreadPtr->start(); + m_audioWorkerPtr->moveToThread(m_workerThreadPtr); // connect worker signals to slots connect(this, &VsAudio::signalStartAudio, m_audioWorkerPtr.get(), @@ -185,6 +185,15 @@ VsAudio::VsAudio(QObject* parent) #endif } +VsAudio::~VsAudio() +{ + if (m_workerThreadPtr == nullptr) + return; + m_workerThreadPtr->quit(); + WaitForSignal(m_workerThreadPtr, &QThread::finished); + m_workerThreadPtr->deleteLater(); +} + bool VsAudio::backendAvailable() const { if constexpr ((isBackendAvailable() diff --git a/src/gui/vsAudio.h b/src/gui/vsAudio.h index 5f37b67..298d211 100644 --- a/src/gui/vsAudio.h +++ b/src/gui/vsAudio.h @@ -143,7 +143,7 @@ class VsAudio : public QObject // Constructor explicit VsAudio(QObject* parent = nullptr); - virtual ~VsAudio() {} + virtual ~VsAudio(); // allow VirtualStudio to get Permissions to bind to QML view VsPermissions& getPermissions() { return *m_permissionsPtr; } @@ -363,7 +363,7 @@ class VsAudio : public QObject // other state not shared with QML QSharedPointer m_permissionsPtr; QScopedPointer m_audioWorkerPtr; - QScopedPointer m_workerThread; + QThread* m_workerThreadPtr; QTimer m_inputClipTimer; QTimer m_outputClipTimer; Meter* m_inputMeterPluginPtr; @@ -379,9 +379,9 @@ class VsAudio : public QObject QStringList m_audioBackendComboModel = {"JACK", "RtAudio"}; QStringList m_feedbackDetectionComboModel = {"Enabled", "Disabled"}; QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"}; - QStringList m_bufferStrategyComboModel = {"Minimal Latency", "Stable Latency", - "Loss Concealment (3)", - "Loss Concealment (4)"}; + QStringList m_bufferStrategyComboModel = { + "Minimal Latency", "Stable Latency", "Loss Concealment (Auto)", + "Loss Concealment (No Worker)", "Loss Concealment (Use Worker)"}; friend class VsAudioWorker; }; diff --git a/src/gui/vsDeeplink.cpp b/src/gui/vsDeeplink.cpp index ed6f41d..53bb88d 100644 --- a/src/gui/vsDeeplink.cpp +++ b/src/gui/vsDeeplink.cpp @@ -37,7 +37,7 @@ #include "vsDeeplink.h" -#include +#include #include #include #include @@ -46,7 +46,7 @@ #include #include -VsDeeplink::VsDeeplink(QCoreApplication* app) : m_deeplink(parseDeeplink(app)) +VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink) { setUrlScheme(); checkForInstance(); @@ -195,21 +195,6 @@ void VsDeeplink::handleDeeplinkRequest() } } -QUrl VsDeeplink::parseDeeplink(QCoreApplication* app) -{ - // Parse command line for deep link - QCommandLineParser parser; - QCommandLineOption deeplinkOption(QStringList() << QStringLiteral("deeplink")); - deeplinkOption.setValueName(QStringLiteral("deeplink")); - parser.addOption(deeplinkOption); - parser.parse(app->arguments()); - if (parser.isSet(deeplinkOption)) { - return QUrl(parser.value(deeplinkOption)); - } else { - return QUrl(""); - } -} - void VsDeeplink::setUrlScheme() { #ifdef _WIN32 diff --git a/src/gui/vsDeeplink.h b/src/gui/vsDeeplink.h index 789ca01..8eb932e 100644 --- a/src/gui/vsDeeplink.h +++ b/src/gui/vsDeeplink.h @@ -38,10 +38,10 @@ #ifndef __VSDEEPLINK_H__ #define __VSDEEPLINK_H__ -#include #include #include #include +#include #include class VsDeeplink : public QObject @@ -50,7 +50,7 @@ class VsDeeplink : public QObject public: // construct with an instance of the application, to parse command line args - VsDeeplink(QCoreApplication* app); + VsDeeplink(const QString& deeplink); // virtual destructor since it inherits from QObject // this is used to unregister url handler @@ -94,9 +94,6 @@ class VsDeeplink : public QObject void handleDeeplinkRequest(); private: - // return string from parsing a deep link request - static QUrl parseDeeplink(QCoreApplication* app); - // sets url scheme for windows machines; does nothing on other platforms static void setUrlScheme(); diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp index d3717be..31e1ad9 100644 --- a/src/gui/vsDevice.cpp +++ b/src/gui/vsDevice.cpp @@ -120,6 +120,9 @@ void VsDevice::registerApp() settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix); settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret); settings.endGroup(); + if (!m_appID.isEmpty()) { + updateState(""); + } reply->deleteLater(); }); @@ -186,6 +189,7 @@ void VsDevice::sendHeartbeat() json.insert(QLatin1String("stddev_rtt"), (qint64)(stats.stdDevRtt * ns_per_ms)); json.insert(QLatin1String("high_latency"), m_audioConfigPtr->getHighLatencyFlag()); + json.insert(QLatin1String("network_outage"), m_networkOutage); // For the internal application UI, ms will suffice. No conversion needed QJsonObject pingStats = {}; @@ -301,15 +305,8 @@ JackTrip* VsDevice::initJackTrip( } m_jackTrip->setBindPorts(bindPort); m_jackTrip->setRemoteClientName(m_appID); - // increment m_bufferStrategy by 1 for array-index mapping - m_jackTrip->setBufferStrategy(bufferStrategy + 1); - if (bufferStrategy == 2 || bufferStrategy == 3) { - // use -q auto3 for loss concealment - m_jackTrip->setBufferQueueLength(-3); - } else { - // use -q auto - m_jackTrip->setBufferQueueLength(-500); - } + m_jackTrip->setBufferStrategy(bufferStrategy); + m_jackTrip->setBufferQueueLength(-500); // use -q auto m_jackTrip->setPeerAddress(studioInfo->host()); m_jackTrip->setPeerPorts(studioInfo->port()); m_jackTrip->setPeerHandshakePort(studioInfo->port()); @@ -325,7 +322,8 @@ JackTrip* VsDevice::initJackTrip( // startJackTrip starts the current jacktrip process if applicable void VsDevice::startJackTrip(const VsServerInfo& studioInfo) { - m_stopping = false; + m_stopping = false; + m_networkOutage = false; updateState(studioInfo.id()); // setup websocket listener diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h index 3c82982..99a2fe6 100644 --- a/src/gui/vsDevice.h +++ b/src/gui/vsDevice.h @@ -77,6 +77,8 @@ class VsDevice : public QObject void startJackTrip(const VsServerInfo& studioInfo); void stopJackTrip(bool isReconnecting = false); void reconcileAgentConfig(QJsonDocument newState); + void setNetworkOutage(bool outage = true) { m_networkOutage = outage; } + bool getNetworkOutage() const { return m_networkOutage; } signals: void updateNetworkStats(QJsonObject stats); @@ -113,8 +115,8 @@ class VsDevice : public QObject QScopedPointer m_jackTrip; QRandomGenerator m_randomizer; QTimer m_sendVolumeTimer; - bool m_highLatencyFlag = false; - bool m_stopping = false; + bool m_networkOutage = false; + bool m_stopping = false; }; #endif // VSDEVICE_H diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index 16bad7b..199a5c7 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "2.0.2"; ///< JackTrip version +constexpr const char* const gVersion = "2.1.0"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/main.cpp b/src/main.cpp index 30ae6bf..6a1a817 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -96,9 +97,11 @@ QCoreApplication* createApplication(int& argc, char* argv[]) // Check for some specific, GUI related command line options. bool forceGui = false; for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--gui") == 0) { + if (strncmp(argv[i], "--gui", 5) == 0 || strncmp(argv[i], "--deeplink", 10) == 0 + || strncmp(argv[i], "--classic-gui", 13) == 0 + || strncmp(argv[i], "jacktrip://", 11) == 0) { forceGui = true; - } else if (strcmp(argv[i], "--test-gui") == 0) { + } else if (strncmp(argv[i], "--test-gui", 10) == 0) { // Command line option to test if the binary has been built with GUI support. // Exits immediately. Exits with an error if GUI support has not been built // in. @@ -152,6 +155,13 @@ QCoreApplication* createApplication(int& argc, char* argv[]) QGuiApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif // QT_VERSION + + // Initialize webengine + QtWebEngineQuick::initialize(); + // TODO: Add support for QtWebView + // qputenv("QT_WEBVIEW_PLUGIN", "native"); + // QtWebView::initialize(); + return new JTApplication(argc, argv); #else // Turn on high DPI support. @@ -168,6 +178,23 @@ QCoreApplication* createApplication(int& argc, char* argv[]) Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif // NO_VS #endif // QT_VERSION + +#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // Enables resource sharing between the OpenGL contexts + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); + // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); + + // QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11); + QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + + // Initialize webengine + QtWebEngineQuick::initialize(); + // TODO: Add support for QtWebView + // qputenv("QT_WEBVIEW_PLUGIN", "native"); + // QtWebView::initialize(); +#endif + return new QApplication(argc, argv); #endif // Q_OS_MACOS #endif // NO_GUI @@ -289,15 +316,6 @@ bool isRunFromCmd() int main(int argc, char* argv[]) { -#ifndef NO_GUI -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QtWebEngineQuick::initialize(); - // TODO: Add support for QtWebView - // qputenv("QT_WEBVIEW_PLUGIN", "native"); - // QtWebView::initialize(); -#endif // NO_VS && QT_VERSION -#endif // NO_GUI - QScopedPointer app(createApplication(argc, argv)); QScopedPointer jackTrip; QScopedPointer udpHub; @@ -347,16 +365,27 @@ int main(int argc, char* argv[]) qmlRegisterType("VS", 1, 0, "Clipboard"); // prepare handler for deeplinks jacktrip://join/ - vsDeeplinkPtr.reset(new VsDeeplink(app.data())); + vsDeeplinkPtr.reset(new VsDeeplink(cliSettings->getDeeplink())); if (!vsDeeplinkPtr->getDeeplink().isEmpty()) { bool readyForExit = vsDeeplinkPtr->waitForReady(); if (readyForExit) return 0; } - // Check if we need to show our first run window. + // Check which mode we are running in QSettings settings; - int uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); + int uiMode = QJackTrip::UNSET; + if (!vsDeeplinkPtr->getDeeplink().isEmpty()) { + uiMode = QJackTrip::VIRTUAL_STUDIO; + } else if (cliSettings->guiForceClassicMode()) { + uiMode = QJackTrip::STANDARD; + // force settings change; otherwise, virtual studio + // window will still be displayed + settings.setValue(QStringLiteral("UiMode"), uiMode); + } else { + uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); + } + window.reset(new QJackTrip(cliSettings, !vsDeeplinkPtr->getDeeplink().isEmpty())); #else window.reset(new QJackTrip(cliSettings)); @@ -369,6 +398,7 @@ int main(int argc, char* argv[]) QObject::connect(vsPtr.data(), &VirtualStudio::signalExit, app.data(), &QCoreApplication::quit, Qt::QueuedConnection); vsPtr->setStandardWindow(window); + vsPtr->setCLISettings(cliSettings); window->setVs(vsPtr); QObject::connect(vsDeeplinkPtr.get(), &VsDeeplink::signalDeeplink, vsPtr.get(), &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection); diff --git a/win/build_installer.bat b/win/build_installer.bat index eade10d..3c5bac5 100755 --- a/win/build_installer.bat +++ b/win/build_installer.bat @@ -17,7 +17,7 @@ xcopy ..\LICENSES deploy\LICENSES\ REM create RTF file with licenses' text set LICENSEPATH=deploy\license.rtf echo {\rtf1\ansi\deff0 {\fonttbl {\f0 Calibri;}} \f0\fs22>%LICENSEPATH% -for %%f in (..\LICENSE.md ..\LICENSES\MIT.txt ..\LICENSES\GPL-3.0.txt ..\LICENSES\LGPL-3.0-only.txt) do ( +for %%f in (..\LICENSE.md ..\LICENSES\MIT.txt ..\LICENSES\GPL-3.0.txt ..\LICENSES\LGPL-3.0-only.txt ..\LICENSES\AVC.txt) do ( for /f "delims=" %%x in ('type %%f') do ( echo %%x\line>>%LICENSEPATH% ) diff --git a/win/jacktrip.exe.manifest b/win/jacktrip.exe.manifest new file mode 100644 index 0000000..3afa605 --- /dev/null +++ b/win/jacktrip.exe.manifest @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/win/meson.build b/win/meson.build index d557456..7362bba 100644 --- a/win/meson.build +++ b/win/meson.build @@ -5,7 +5,8 @@ if host_machine.system() == 'windows' depend_files: 'jacktrip.ico', include_directories: '.') - defines += '-D_WIN32_WINNT=0x0600' + defines += '-D_WIN32_WINNT=0x0A00' + defines += '-DWINVER=0x0A00' defines += '-DWIN32_LEAN_AND_MEAN' defines += '-DNOMINMAX' diff --git a/win/qjacktrip.rc b/win/qjacktrip.rc index 9516335..8d7ab5a 100644 --- a/win/qjacktrip.rc +++ b/win/qjacktrip.rc @@ -1 +1,6 @@ IDI_ICON1 ICON "jacktrip.ico" + +#ifndef RT_MANIFEST +#define RT_MANIFEST 24 +#endif +1 RT_MANIFEST "jacktrip.exe.manifest" \ No newline at end of file diff --git a/win/qt6-noguids.wxs b/win/qt6-noguids.wxs index b77394a..41fa3fa 100644 --- a/win/qt6-noguids.wxs +++ b/win/qt6-noguids.wxs @@ -2,18 +2,15 @@ + + - - - - - @@ -99,6 +96,9 @@ + + + @@ -111,6 +111,9 @@ + + + @@ -144,6 +147,9 @@ + + + @@ -159,6 +165,9 @@ + + + @@ -174,1436 +183,1622 @@ + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1623,9 +1818,6 @@ - - - @@ -1803,123 +1995,158 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1928,58 +2155,63 @@ - - + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + \ No newline at end of file diff --git a/win/qt6.wxs b/win/qt6.wxs index ed9ced1..8231b35 100644 --- a/win/qt6.wxs +++ b/win/qt6.wxs @@ -2,20 +2,16 @@ + + - - - - - - @@ -89,6 +85,9 @@ + + + @@ -101,6 +100,9 @@ + + + @@ -134,6 +136,9 @@ + + + @@ -149,6 +154,9 @@ + + + @@ -164,1436 +172,1622 @@ + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1784,123 +1978,158 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1909,58 +2138,63 @@ - - + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + \ No newline at end of file