set(novs TRUE)
endif ()
+set(QRC_FILE "src/images/images.qrc")
if (novs)
add_compile_definitions(NO_VS)
- set(QRC_FILE "src/gui/qjacktrip_novs.qrc")
else ()
- set(QRC_FILE "src/gui/qjacktrip.qrc")
+ set(QRC_FILE "src/vs/vs.qrc")
endif ()
if (vsftux)
src/gui/textbuf.cpp
src/gui/vuMeter.cpp
src/Meter.cpp
+ src/UserInterface.cpp
)
if (NOT novs)
set (qjacktrip_SRC ${qjacktrip_SRC}
- src/gui/virtualstudio.cpp
- src/gui/vsApi.cpp
- src/gui/vsAuth.cpp
- src/gui/vsDeviceCodeFlow.cpp
- src/gui/vsDeeplink.cpp
- src/gui/vsQuickView.cpp
- src/gui/vsServerInfo.cpp
- src/gui/vsPing.cpp
- src/gui/vsPinger.cpp
- src/gui/vsDevice.cpp
- src/gui/vsAudio.cpp
- src/gui/vsWebSocket.cpp
- src/gui/vsPermissions.cpp
- src/gui/qjacktrip.qrc
+ src/vs/virtualstudio.cpp
+ src/vs/vsApi.cpp
+ src/vs/vsAuth.cpp
+ src/vs/vsDeviceCodeFlow.cpp
+ src/vs/vsDeeplink.cpp
+ src/vs/vsQuickView.cpp
+ src/vs/vsServerInfo.cpp
+ src/vs/vsPing.cpp
+ src/vs/vsPinger.cpp
+ src/vs/vsDevice.cpp
+ src/vs/vsAudio.cpp
+ src/vs/vsWebSocket.cpp
+ src/vs/vsPermissions.cpp
+ src/vs/vs.qrc
+ src/images/images.qrc
src/Analyzer.cpp
src/Monitor.cpp
src/Volume.cpp
src/Tone.cpp
# Need to include this for AUTOMOC to do its thing
- src/JTApplication.h
- src/gui/vsQmlClipboard.h
+ src/vs/JTApplication.h
+ src/vs/vsQmlClipboard.h
)
if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
- set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/vsMacPermissions.mm)
+ set (qjacktrip_SRC ${qjacktrip_SRC} src/vs/vsMacPermissions.mm)
endif ()
else ()
- set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/qjacktrip_novs.qrc)
+ set (qjacktrip_SRC ${qjacktrip_SRC} src/images/images.qrc)
endif ()
if (NOT noupdater)
file(WRITE "win/qjacktrip.rc" "${RC_CONTENTS}")
set (qjacktrip_SRC ${qjacktrip_SRC} win/qjacktrip.rc)
elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
- set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/NoNap.mm)
+ set (qjacktrip_SRC ${qjacktrip_SRC} src/NoNap.mm)
set (CMAKE_C_FLAGS "-x objective-c")
set (CMAKE_EXE_LINKER_FLAGS "-framework Foundation")
if (NOT novs)
# JackTrip License
-Copyright © 2008-2020 Juan-Pablo Caceres, Chris Chafe.
+Copyright © 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al.
SoundWIRE group at CCRMA, Stanford University.
+Virtual Studio interface and integration,
+Copyright © 2022-2024 JackTrip Labs, Inc.
+
Classic mode graphical user interface originally released as QJackTrip,
Copyright © 2020 Aaron Wyatt
-Virtual Studio interface and integration
-Copyright © 2022-2023 JackTrip Labs, Inc.
+The JackTrip project including Virtual Studio interface and integration
+is open source distributed under the MIT license. The Classic mode
+graphical interface is open source distributed under a GPL license.
+
+JackTrip uses the Qt library. Qt's source code can be downloaded from
+[https://download.qt.io/official_releases/qt/](https://download.qt.io/official_releases/qt/).
-JackTrip project consists of files under MIT and GPL licenses, indicated in the
-header of individual files. Early versions of JackTrip were licensed under MIT.
+Unsigned builds provided on GitHub's Releases page include the Classic
+mode graphical interface and use an open source distribution of Qt.
+These are distributed under a GPL license.
-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.
+Signed builds for Windows and Mac provided by JackTrip Labs do not
+include the Classic mode graphical interface and use a commercial
+Qt license. These are distributed under a MIT license.
Windows builds of JackTrip may include support for ASIO.
ASIO is a trademark and software of Steinberg Media Technologies GmbH.
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/).
+The text of individual licenses is provided in the `LICENSES/` folder.
+
- Copyright (c) 2020 Juan-Pablo Caceres, Chris Chafe.
+ Copyright (c) 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al.
SoundWIRE group at CCRMA, Stanford University.
+ Virtual Studio interface and integration
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
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
+- Version: "2.4.0"
+ Date: 2024-09-13
+ Description:
+ - (addded) qWave Quality of Service for Windows users
+ - (addded) VS Mode warning when speakers selected for output
+ - (addded) VS Mode dialog when connecting with audio warning
+ - (updated) PLC auto headroom tweaks to improve audio quality
+ - (updated) Increased UDP timeout from 30 to 50 milliseconds
+ - (updated) MIT license for VS mode interface and integration
+ - (updated) Removed classic mode from JackTrip Labs builds
+ - (updated) ASIO is now only enabled for JackTrip Labs builds
+ - (updated) VS Mode now only supports buffer strategy 3 (PLC)
+ - (updated) VS Mode removed button click to create first studio
+ - (updated) VS Mode updated to use new studio creation interface
+ - (updated) VS Mode updated to use new user profile interface
+ - (updated) VS Mode changed red text color for buttons to black
+ - (updated) VS Mode improved error handling for login screen
+ - (updated) VS Mode improved messaging while loading studios
+ - (fixed) VS Mode possible failures when loading studios
+ - (fixed) VS Mode studio refresh updated to avoid jumpiness
+ - (fixed) VS Mode blacklisted iPhone microphone device
+ - (fixed) Race condition in automatic patching for JACK
+ - (fixed) Missing files in Linux binary zip file
- Version: "2.3.1"
Date: 2024-07-26
Description:
src/gui/textbuf.h \
src/gui/vuMeter.h
!novs {
- HEADERS += src/gui/virtualstudio.h \
- src/gui/vsApi.h \
- src/gui/vsAuth.h \
- src/gui/vsDeviceCodeFlow.h \
- src/gui/vsDeeplink.h \
- src/gui/vsDevice.h \
- src/gui/vsAudio.h \
- src/gui/vsServerInfo.h \
- src/gui/vsQuickView.h \
- src/gui/vsWebSocket.h \
- src/gui/vsPermissions.h \
- src/gui/vsPinger.h \
- src/gui/vsPing.h \
- src/gui/vsQmlClipboard.h \
- src/JTApplication.h
+ HEADERS += src/vs/virtualstudio.h \
+ src/vs/vsApi.h \
+ src/vs/vsAuth.h \
+ src/vs/vsDeviceCodeFlow.h \
+ src/vs/vsDeeplink.h \
+ src/vs/vsDevice.h \
+ src/vs/vsAudio.h \
+ src/vs/vsServerInfo.h \
+ src/vs/vsQuickView.h \
+ src/vs/vsWebSocket.h \
+ src/vs/vsPermissions.h \
+ src/vs/vsPinger.h \
+ src/vs/vsPing.h \
+ src/vs/vsQmlClipboard.h \
+ src/vs/JTApplication.h
}
!noupdater:!linux-g++:!linux-g++-64 {
HEADERS += src/dblsqd/feed.h \
src/gui/qjacktrip.cpp \
src/gui/about.cpp \
src/gui/textbuf.cpp \
- src/gui/vuMeter.cpp
+ src/gui/vuMeter.cpp \
+ src/UserInterface.cpp
!novs {
- SOURCES += src/gui/virtualstudio.cpp \
- src/gui/vsApi.cpp \
- src/gui/vsAuth.cpp \
- src/gui/vsDeviceCodeFlow.cpp \
- src/gui/vsDeeplink.cpp \
- src/gui/vsDevice.cpp \
- src/gui/vsAudio.cpp \
- src/gui/vsServerInfo.cpp \
- src/gui/vsQuickView.cpp \
- src/gui/vsWebSocket.cpp \
- src/gui/vsPermissions.cpp \
- src/gui/vsPinger.cpp \
- src/gui/vsPing.cpp
+ SOURCES += src/vs/virtualstudio.cpp \
+ src/vs/vsApi.cpp \
+ src/vs/vsAuth.cpp \
+ src/vs/vsDeviceCodeFlow.cpp \
+ src/vs/vsDeeplink.cpp \
+ src/vs/vsDevice.cpp \
+ src/vs/vsAudio.cpp \
+ src/vs/vsServerInfo.cpp \
+ src/vs/vsQuickView.cpp \
+ src/vs/vsWebSocket.cpp \
+ src/vs/vsPermissions.cpp \
+ src/vs/vsPinger.cpp \
+ src/vs/vsPing.cpp
}
!noupdater:!linux-g++:!linux-g++-64 {
SOURCES += src/dblsqd/feed.cpp \
!nogui {
macx {
- HEADERS += src/gui/NoNap.h
- OBJECTIVE_SOURCES += src/gui/NoNap.mm
+ HEADERS += src/NoNap.h
+ OBJECTIVE_SOURCES += src/NoNap.mm
!novs {
- HEADERS += src/gui/vsMacPermissions.h
- OBJECTIVE_SOURCES += src/gui/vsMacPermissions.mm
+ HEADERS += src/vs/vsMacPermissions.h
+ OBJECTIVE_SOURCES += src/vs/vsMacPermissions.mm
}
}
FORMS += src/gui/qjacktrip.ui \
src/gui/about.ui \
src/gui/messageDialog.ui
- novs {
- RESOURCES += src/gui/qjacktrip_novs.qrc
- } else {
- RESOURCES += src/gui/qjacktrip.qrc
+ RESOURCES += src/images/images.qrc
+ !novs {
+ RESOURCES += src/vs/vs.qrc
}
!noupdater:!linux-g++:!linux-g++-64 {
FORMS += src/dblsqd/update_dialog.ui
fi
DEPLOY_OPTS="-executable=$APPNAME.app/Contents/MacOS/jacktrip -libpath=$QT_PATH/lib"
if [ -n "$DYNAMIC_VS" ]; then
- DEPLOY_OPTS="$DEPLOY_OPTS -qmldir=../src/gui"
+ DEPLOY_OPTS="$DEPLOY_OPTS -qmldir=../src/vs"
fi
$DEPLOY_CMD "$APPNAME.app" $DEPLOY_OPTS
endif
endif
-if get_option('nogui') == true
+if qt_version == '5'
+ deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
+else
+ deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system')
+endif
+
+if get_option('nogui') == true or (get_option('noclassic') == true and get_option('novs') == true)
+ # command line only
defines += '-DNO_GUI'
+else
+ # include vs and/or classic gui
if qt_version == '5'
- deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
+ deps += dependency('qt5', modules: ['Gui', 'Widgets'], include_type: 'system')
else
- deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system')
+ deps += dependency('qt6', modules: ['Gui', 'Widgets'], include_type: 'system')
+ endif
+ qres = ['src/images/images.qrc']
+ src += 'src/UserInterface.cpp'
+
+ if get_option('noclassic') == true
+ defines += '-DNO_CLASSIC'
+ else
+ # support classic mode
+ src += [
+ 'src/gui/qjacktrip.cpp',
+ 'src/gui/about.cpp',
+ 'src/gui/messageDialog.cpp',
+ 'src/gui/textbuf.cpp',
+ 'src/gui/vuMeter.cpp'
+ ]
+ moc_h += [
+ 'src/gui/about.h',
+ 'src/gui/qjacktrip.h',
+ 'src/gui/messageDialog.h',
+ 'src/gui/textbuf.h',
+ 'src/gui/vuMeter.h'
+ ]
+ ui_h += [
+ 'src/gui/qjacktrip.ui',
+ 'src/gui/messageDialog.ui',
+ 'src/gui/about.ui'
+ ]
endif
-else
- src += [
- 'src/gui/qjacktrip.cpp',
- 'src/gui/about.cpp',
- 'src/gui/messageDialog.cpp',
- 'src/gui/textbuf.cpp',
- 'src/gui/vuMeter.cpp'
- ]
- moc_h += [
- 'src/gui/about.h',
- 'src/gui/qjacktrip.h',
- 'src/gui/messageDialog.h',
- 'src/gui/textbuf.h',
- 'src/gui/vuMeter.h'
- ]
- ui_h += [
- 'src/gui/qjacktrip.ui',
- 'src/gui/messageDialog.ui',
- 'src/gui/about.ui'
- ]
if get_option('novs') == true
defines += '-DNO_VS'
- if qt_version == '5'
- deps += dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
- else
- deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
- endif
- qres = ['src/gui/qjacktrip_novs.qrc']
else
src += [
- 'src/gui/virtualstudio.cpp',
- 'src/gui/vsAuth.cpp',
- 'src/gui/vsApi.cpp',
- 'src/gui/vsDeviceCodeFlow.cpp',
- 'src/gui/vsDeeplink.cpp',
- 'src/gui/vsDevice.cpp',
- 'src/gui/vsAudio.cpp',
- 'src/gui/vsServerInfo.cpp',
- 'src/gui/vsQuickView.cpp',
- 'src/gui/vsWebSocket.cpp',
- 'src/gui/vsPermissions.cpp',
- 'src/gui/vsPinger.cpp',
- 'src/gui/vsPing.cpp',
- 'src/gui/WebSocketTransport.cpp'
+ 'src/vs/virtualstudio.cpp',
+ 'src/vs/vsAuth.cpp',
+ 'src/vs/vsApi.cpp',
+ 'src/vs/vsDeviceCodeFlow.cpp',
+ 'src/vs/vsDeeplink.cpp',
+ 'src/vs/vsDevice.cpp',
+ 'src/vs/vsAudio.cpp',
+ 'src/vs/vsServerInfo.cpp',
+ 'src/vs/vsQuickView.cpp',
+ 'src/vs/vsWebSocket.cpp',
+ 'src/vs/vsPermissions.cpp',
+ 'src/vs/vsPinger.cpp',
+ 'src/vs/vsPing.cpp',
+ 'src/vs/WebSocketTransport.cpp'
]
moc_h += [
- 'src/gui/virtualstudio.h',
- 'src/gui/vsApi.h',
- 'src/gui/vsAuth.h',
- 'src/gui/vsDeviceCodeFlow.h',
- 'src/gui/vsDeeplink.h',
- 'src/gui/vsDevice.h',
- 'src/gui/vsAudio.h',
- 'src/gui/vsServerInfo.h',
- 'src/gui/vsQuickView.h',
- 'src/gui/vsWebSocket.h',
- 'src/gui/vsPermissions.h',
- 'src/gui/vsPinger.h',
- 'src/gui/vsPing.h',
- 'src/gui/vsQmlClipboard.h',
- 'src/JTApplication.h',
- 'src/gui/WebSocketTransport.h'
+ 'src/vs/virtualstudio.h',
+ 'src/vs/vsApi.h',
+ 'src/vs/vsAuth.h',
+ 'src/vs/vsDeviceCodeFlow.h',
+ 'src/vs/vsDeeplink.h',
+ 'src/vs/vsDevice.h',
+ 'src/vs/vsAudio.h',
+ 'src/vs/vsServerInfo.h',
+ 'src/vs/vsQuickView.h',
+ 'src/vs/vsWebSocket.h',
+ 'src/vs/vsPermissions.h',
+ 'src/vs/vsPinger.h',
+ 'src/vs/vsPing.h',
+ 'src/vs/vsQmlClipboard.h',
+ 'src/vs/JTApplication.h',
+ 'src/vs/WebSocketTransport.h'
]
if host_machine.system() == 'darwin'
- moc_h += ['src/gui/vsMacPermissions.h']
+ moc_h += ['src/vs/vsMacPermissions.h']
endif
- if get_option('vsftux') == true
+ if get_option('vsftux') == true or get_option('noclassic') == true
defines += '-DVS_FTUX'
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']
+ deps += dependency('qt6', modules: ['Core5Compat', 'Quick', 'QuickControls2', 'Qml', 'ShaderTools', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system')
+ qres += ['src/vs/vs.qrc']
endif
if get_option('noupdater') == true or host_machine.system() == 'linux'
]
moc_h += [
'src/dblsqd/feed.h',
- 'src/dblsqd/release.h',
- 'src/dblsqd/semver.h',
'src/dblsqd/update_dialog.h'
]
ui_h += ['src/dblsqd/update_dialog.ui']
endif
endif
-# TODO: QT_OPENSOURCE should only be defined for open source Qt distribution
-# in QMake this can be checked with QT_EDITION == 'OpenSource'
-defines += '-DQT_OPENSOURCE'
+# QT_OPENSOURCE should only be defined for open source Qt distribution
+if get_option('qtedition') != 'commercial'
+ defines += '-DQT_OPENSOURCE'
+endif
rtaudio_dep = dependency('rtaudio', required: get_option('rtaudio'))
if rtaudio_dep.found() == true
endif
if host_machine.system() == 'darwin'
- src += ['src/gui/NoNap.mm']
+ src += ['src/NoNap.mm']
# Adding CoreAudio here is a workaround and should be removed
# when https://github.com/thestk/rtaudio/issues/302 is fixed
# and arrived in all common package managers
add_languages('objcpp')
endif
-if host_machine.system() == 'darwin' and get_option('novs') == false
- src += ['src/gui/vsMacPermissions.mm']
+if host_machine.system() == 'darwin' and get_option('novs') == false and get_option('nogui') == false
+ src += ['src/vs/vsMacPermissions.mm']
apple_av_dep = dependency('appleframeworks', modules : ['avfoundation', 'webkit'])
deps += apple_av_dep
endif
option('weakjack', type : 'boolean', value : false, description: 'Weak link JACK library')
option('nogui', type : 'boolean', value : false, description: 'Build without graphical user interface')
option('novs', type : 'boolean', value : false, description: 'Build without Virtual Studio support')
+option('noclassic', type : 'boolean', value : false, description: 'Build without classic mode support')
option('vsftux', type : 'boolean', value : false, description: 'Build with Virtual Studio first launch experience')
option('noupdater', type : 'boolean', value : false, description: 'Build without auto-update support')
option('nofeedback', type : 'boolean', value : false, description: 'Build without feedback detection')
option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly')
option('qtversion', type : 'combo', choices: ['', '5', '6'], description: 'Choose to build with either Qt5 or Qt6')
+option('qtedition', type : 'combo', choices: ['opensource', 'commercial'], description: 'Choose license edition for Qt')
option('buildinfo', type : 'string', value : '', yield : true, description: 'Additional info used to describe the build')
\ No newline at end of file
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
mWarningMsg =
"The buffer size setting for your audio device will cause high latency "
"or audio delay. Use an audio device that supports small buffer sizes "
- "to reduce audio delays. Click for more info.";
+ "to reduce audio delays.";
mWarningHelpUrl = "https://support.jacktrip.com/recommended-audio-interfaces";
mHighLatencyFlag = true;
break;
mWarningMsg =
"You audio device drivers may cause high latency or audio delay. Install "
"and use ASIO drivers provided by your device's manufacturer to reduce "
- "audio delays. Click for more info.";
+ "audio delays.";
mWarningHelpUrl =
"https://support.jacktrip.com/troubleshooting-windows-drivers-and-asio";
mHighLatencyFlag = true;
mWarningHelpUrl = "";
mHighLatencyFlag = true;
break;
+ case DEVICE_WARN_SPEAKERS:
+ mWarningMsg =
+ "You appear to have selected speakers for audio output. "
+ "Using speakers with microphones will likely cause a loud feedback "
+ "loop. We strongly recommend that you use wired headphones instead.";
+ mWarningHelpUrl = "https://support.jacktrip.com/recommended-audio-interfaces";
+ break;
default:
mWarningMsg = "";
mWarningHelpUrl = "";
DEVICE_WARN_NONE,
DEVICE_WARN_BUFFER_LATENCY,
DEVICE_WARN_ASIO_LATENCY,
- DEVICE_WARN_ALSA_LATENCY
+ DEVICE_WARN_ALSA_LATENCY,
+ DEVICE_WARN_SPEAKERS
};
enum errorMessageT {
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 JTApplication.h
- * \author Matt Hortoon
- * \date July 2022
- */
-
-#ifndef JTAPPLICATION_H
-#define JTAPPLICATION_H
-
-#include <QApplication>
-#include <QDebug>
-#include <QDesktopServices>
-#include <QEvent>
-#include <QFileOpenEvent>
-#include <QObject>
-
-class JTApplication : public QApplication
-{
- Q_OBJECT
-
- public:
- JTApplication(int& argc, char** argv) : QApplication(argc, argv) {}
-
- bool event(QEvent* event) override
- {
- if (event->type() == QEvent::FileOpen) {
- QFileOpenEvent* openEvent = static_cast<QFileOpenEvent*>(event);
-
- QDesktopServices::openUrl(openEvent->url());
- }
- return QApplication::event(event);
- }
-};
-
-#endif // JTAPPLICATION_H
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2020 Aaron Wyatt.
+
+ 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.
+*/
+//*****************************************************************
+
+#ifndef __NONAP_H__
+#define __NONAP_H__
+
+#include <objc/objc.h>
+
+class NoNap
+{
+ public:
+ NoNap();
+ ~NoNap();
+
+ void disableNap();
+ void enableNap();
+
+ private:
+ id m_activity;
+ bool m_preventNap;
+};
+
+#endif // __NONAP_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2020 Aaron Wyatt.
+
+ 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.
+*/
+//*****************************************************************
+
+#include "NoNap.h"
+#include <Foundation/Foundation.h>
+
+NoNap::NoNap() :
+ m_preventNap(false)
+{}
+
+void NoNap::disableNap()
+{
+ if (m_preventNap) {
+ return;
+ }
+ m_preventNap = true;
+ m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical | NSActivityUserInitiated reason:@"Disable App Nap"];
+ [m_activity retain];
+}
+
+void NoNap::enableNap()
+{
+ if (!m_preventNap) {
+ return;
+ }
+ m_preventNap = false;
+ [[NSProcessInfo processInfo] endActivity:m_activity];
+ [m_activity release];
+}
+
+NoNap::~NoNap()
+{
+ if (m_preventNap) {
+ enableNap();
+ }
+}
{
QMutexLocker locker(&m_connectionMutex);
+ // this works around a JACK timing bug found under pipewire
+ // if registerClient is called for a second (or subsequent) hub client
+ // jack_client won't have properly updated its ports
+ // the workaround is to sleep here and let JACK update
+ if (m_jackClient)
+ QThread::msleep(100);
+
// If our jack client isn't running, start it.
if (!m_jackClient) {
m_jackClient = jack_client_open("jthubpatcher", JackNoStartServer, &m_status);
// tweak
constexpr int WindowDivisor = 8; // for faster auto tracking
constexpr double AutoHeadroomGlitchTolerance =
- 0.02; // Acceptable rate of skips before auto headroom is increased (2.0%)
+ 0.01; // Acceptable rate of skips before auto headroom is increased (1.0%)
constexpr double AutoHistoryWindow =
60; // rolling window of time (in seconds) over which auto tolerance roughly adjusts
constexpr double AutoSmoothingFactor =
if (mAutoHeadroom < 0) {
// variable headroom: automatically increase to minimize glitch counts
// only increase headroom if doing so would have reduced the number of
- // glitches that occured over the past two seconds by 2% or more.
+ // glitches that occured over the past second by 1% or more.
// prevent headroom from growing beyond rolling average of max.
const int skipsAllowed =
static_cast<int>(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP);
if (mSkipAutoHeadroom) {
mSkipAutoHeadroom = false;
} else {
+ // don't increase headroom two intervals in a row
mSkipAutoHeadroom = true;
++mCurrentHeadroom;
cout << "PLC glitches=" << glitches << " skipped=" << skipped << ">"
<< " (max=" << pushStat->longTermMax << ")" << endl;
}
} else {
- mSkipAutoHeadroom = true;
+ // require 2 seconds in a row if headroom >= two packet intervals
+ mSkipAutoHeadroom = mMsecTolerance >= (mPeerFPPdurMsec * 2);
}
} else {
// fixed headroom
if (skipped < 0)
skipped += NumSlots;
}
- if (mIncomingTiming[next] + mMsecTolerance >= now) {
+ // check if packet's age matches tolerance, or is the best candidate we have
+ if (mIncomingTiming[next] + mMsecTolerance >= now || i == 0) {
// next is the best candidate
memcpy(mXfrBuffer, mSlots[next], mPeerBytes);
mLastSeqNumOut = next;
throw std::runtime_error(errorMsg.toStdString());
}
+ // provide warnings for common known failure cases
+ const QString out_device_lower_name =
+ QString::fromStdString(out_device.name).toLower();
+ if (out_device_lower_name.contains("speakers")
+ || out_device_lower_name.contains("lautsprecher")) {
+ AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_SPEAKERS);
+ }
+
if (in_device.api == out_device.api) {
#ifdef _WIN32
if (in_device.api != RtAudio::WINDOWS_ASIO) {
#include "jacktrip_globals.h"
#ifdef _WIN32
// #include <winsock.h>
+#include <qos2.h>
#include <stdio.h>
#include <winsock2.h> //cc need SD_SEND
#pragma comment(lib, "ws2_32.lib")
}
}
-#if defined(_WIN32)
-void UdpDataProtocol::setSocket(SOCKET& socket)
-#else
-void UdpDataProtocol::setSocket(int& socket)
-#endif
+void UdpDataProtocol::setSocket(socket_type& socket)
{
// If we haven't been passed a valid socket, then we should bind one.
#if defined(_WIN32)
}
//*******************************************************************************
-#if defined(_WIN32)
-SOCKET UdpDataProtocol::bindSocket()
-#else
-int UdpDataProtocol::bindSocket()
-#endif
+socket_type UdpDataProtocol::bindSocket()
{
QMutexLocker locker(&sUdpMutex);
::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
#endif
+ // set qos for windows after flow is established (requires peer address/port)
+ if (setSocketQos(sock_fd)) {
+ std::cout << "Set QoS for network socket" << std::endl;
+ } else {
+ std::cerr << "Failed to set QoS for network socket" << std::endl;
+ }
+
+ // Bind the Socket
+ if (mIPv6) {
+ if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) {
+ throw std::runtime_error("ERROR: UDP Socket Bind Error");
+ }
+ } else {
+ if ((::bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr))) < 0) {
+ throw std::runtime_error("ERROR: UDP Socket Bind Error");
+ }
+ }
+
+ // Return our file descriptor so the socket can be shared for a
+ // full duplex connection.
+ return sock_fd;
+}
+
+bool UdpDataProtocol::setSocketQos(socket_type& sock_fd)
+{
#if defined(_WIN32)
- // TODO: these don't seem to work on windows. we likely need to use qWAVE or qos2
+ // Windows QoS (qWave) for audio traffic flows
+ // https://learn.microsoft.com/en-us/windows/win32/api/_qos/
+ // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/qos/qwave-api-reference
+
+ // Initialize the QoS version parameter.
+ QOS_VERSION Version;
+ Version.MajorVersion = 1;
+ Version.MinorVersion = 0;
+
+ // Get a handle to the QoS subsystem.
+ HANDLE QoSHandle = NULL;
+ BOOL QoSResult = QOSCreateHandle(&Version, &QoSHandle);
+ if (QoSResult != TRUE) {
+ std::cerr << "QOSCreateHandle failed. Error: " << WSAGetLastError() << std::endl;
+ return false;
+ }
+
+ // Add socket to flow.
+ QOS_FLOWID QoSFlowId = 0; // Flow Id must be 0.
+ PSOCKADDR pSockAddr;
+ if (mIPv6) {
+ pSockAddr = reinterpret_cast<PSOCKADDR>(&mPeerAddr6);
+ } else {
+ pSockAddr = reinterpret_cast<PSOCKADDR>(&mPeerAddr);
+ }
+ // Note: QOSTrafficTypeVoice sets DSCP to 56 (high VO for WMM)
+ // without having to call QOSSetFlow(). This is best for voice.
+ QoSResult = QOSAddSocketToFlow(QoSHandle, sock_fd, pSockAddr, QOSTrafficTypeVoice,
+ QOS_NON_ADAPTIVE_FLOW, &QoSFlowId);
+ if (QoSResult != TRUE) {
+ std::cerr << "QOSAddSocketToFlow failed. Error: ";
+ std::cerr << WSAGetLastError() << std::endl;
+ return false;
+ }
#elif defined(__APPLE__)
// set service type "Interactive Voice"
// TODO: this is supposed to be the right thing to do on OSX, but doesn't seem to do
// anything
const int val = NET_SERVICE_TYPE_VO;
- ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val));
+ int result =
+ ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val));
+ if (result != 0) {
+ std::cerr << "setsockopt failed. Error: " << errno << std::endl;
+ return false;
+ }
#else
- // Set ToS to DSCP Expedited Forwarding (EF), recommended for Audio
+ // Set ToS to DSCP 56 (high VO for WMM), recommended for Audio
// See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474
// See also
// https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header
- const char tos = 0xB8; // 10111000
+ const char tos = 0xE0; // 11100000 (56 << 2)
+ int result;
if (mIPv6) {
- ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos));
+ result = ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos));
} else {
- ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+ result = ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+ }
+ if (result != 0) {
+ std::cerr << "setsockopt failed. Error: " << errno << std::endl;
+ return false;
}
// Set 802.1q QoS priority
int priority = 6;
- ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
-#endif
-
- // Bind the Socket
- if (mIPv6) {
- if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) {
- throw std::runtime_error("ERROR: UDP Socket Bind Error");
- }
- } else {
- if ((::bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr))) < 0) {
- throw std::runtime_error("ERROR: UDP Socket Bind Error");
- }
+ result = ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
+ if (result != 0) {
+ std::cerr << "setsockopt failed. Error: " << errno << std::endl;
+ return false;
}
+#endif
- // Return our file descriptor so the socket can be shared for a
- // full duplex connection.
- return sock_fd;
+ return true;
}
void UdpDataProtocol::processControlPacket(const char* buf)
#include "jacktrip_globals.h"
#include "jacktrip_types.h"
+#if defined(_WIN32)
+typedef SOCKET socket_type;
+#else
+typedef int socket_type;
+#endif
+
/** \brief UDP implementation of DataProtocol class
*
* The class has a <tt>bind port</tt> and a <tt>peer port</tt>. The meaning of these
*/
void setPeerAddress(const char* peerHostOrIP);
-#if defined(_WIN32)
- void setSocket(SOCKET& socket);
-#else
- void setSocket(int& socket);
-#endif
+ void setSocket(socket_type& socket);
void processControlPacket(const char* buf);
protected:
/** \brief Binds the UDP socket to the available address and specified port
*/
-#if defined(_WIN32)
- SOCKET bindSocket();
-#else
- int bindSocket();
-#endif
+ socket_type bindSocket();
+
+ /** \brief Setup QoS for the network socket/flow
+ */
+ bool setSocketQos(socket_type& sock_fd);
/** \brief This function blocks until data is available for reading in the
* socket. The function will timeout after timeout_msec microseconds.
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024 Michael Dickey, Aaron Wyatt.
+
+ 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 UserInterface.cpp
+ * \author Michael Dickey, Aaron Wyatt
+ * \date August 2024
+ */
+
+#include "UserInterface.h"
+
+#include <QSettings>
+
+#ifndef NO_VS
+#include "vs/virtualstudio.h"
+#endif // NO_VS
+
+#ifndef NO_CLASSIC
+#include "gui/qjacktrip.h"
+#endif // NO_CLASSIC
+
+#if !defined(NO_UPDATER) && !defined(__unix__)
+#include "dblsqd/feed.h"
+#include "dblsqd/update_dialog.h"
+#endif // !defined(NO_UPDATER) && !defined(__unix__)
+
+#ifdef _WIN32
+#include <psapi.h>
+#include <tlhelp32.h>
+#include <windows.h>
+
+bool isRunFromCmd()
+{
+ // Get our parent process pid
+ HANDLE h = NULL;
+ PROCESSENTRY32 pe;
+ ZeroMemory(&pe, sizeof(PROCESSENTRY32));
+ DWORD pid = GetCurrentProcessId();
+ DWORD ppid = 0;
+ pe.dwSize = sizeof(PROCESSENTRY32);
+ h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (Process32First(h, &pe)) {
+ do {
+ // Loop through the list of processes until we find ours.
+ if (pe.th32ProcessID == pid) {
+ ppid = pe.th32ParentProcessID;
+ break;
+ }
+ } while (Process32Next(h, &pe));
+ }
+ CloseHandle(h);
+
+ // Get the name of our parent process;
+ char pname[MAX_PATH] = {0};
+ DWORD size = MAX_PATH;
+ h = NULL;
+ h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ppid);
+ if (h) {
+ if (QueryFullProcessImageNameA(h, 0, pname, &size)) {
+ CloseHandle(h);
+
+ // Check if our parent process is a command line.
+ if (size >= 14 && strncmp(pname + size - 14, "powershell.exe", 14) == 0) {
+ return true;
+ }
+ if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) {
+ return true;
+ }
+ if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) {
+ return true;
+ }
+ // a few extras for msys/cygwin/etc
+ if (size >= 8 && strncmp(pname + size - 8, "bash.exe", 8) == 0) {
+ return true;
+ }
+ if (size >= 6 && strncmp(pname + size - 6, "sh.exe", 6) == 0) {
+ return true;
+ }
+ if (size >= 7 && strncmp(pname + size - 7, "zsh.exe", 7) == 0) {
+ return true;
+ }
+ } else {
+ CloseHandle(h);
+ }
+ }
+
+ return false;
+}
+#endif // _WIN32
+
+UserInterface::UserInterface(QSharedPointer<Settings>& settings) : m_cliSettings(settings)
+{
+}
+
+UserInterface::~UserInterface()
+{
+#ifndef NO_VS
+ m_vs_ui.clear();
+#endif
+#ifndef NO_CLASSIC
+ m_classic_ui.clear();
+#endif
+}
+
+QCoreApplication* UserInterface::createApplication(int& argc, char* argv[])
+{
+#if defined(__unix__)
+ // Check if X or Wayland environment variables are set.
+ if (std::getenv("WAYLAND_DISPLAY") == nullptr && std::getenv("DISPLAY") == nullptr) {
+ std::cout << "ERROR: Display not found. Make sure X or Wayland is running or "
+ "try running jacktrip in command line mode."
+ << std::endl;
+ std::cout << "(To display a list of command line options run \"jacktrip -h\")"
+ << std::endl;
+ std::exit(1);
+ }
+#endif
+
+ QCoreApplication* app;
+#ifdef NO_VS
+ app = QJackTrip::createApplication(argc, argv);
+#else
+ app = VirtualStudio::createApplication(argc, argv);
+#endif
+ app->setOrganizationName(QStringLiteral("jacktrip"));
+ app->setOrganizationDomain(QStringLiteral("jacktrip.org"));
+ app->setApplicationName(QStringLiteral("JackTrip"));
+ app->setApplicationVersion(gVersion);
+
+ return app;
+}
+
+void UserInterface::start(QApplication* app)
+{
+#ifdef _WIN32
+ // Remove the console that appears if we're on windows and not running from a
+ // command line.
+ if (!isRunFromCmd()) {
+ std::cout << "This extra window is caused by a bug in Microsoft Windows. "
+ << "It can safely be ignored or closed." << std::endl
+ << std::endl
+ << "To fix this bug, please upgrade to the latest version of "
+ << "Windows Terminal available in the Microsoft App Store:" << std::endl
+ << "https://aka.ms/terminal" << std::endl;
+
+ FreeConsole();
+ }
+#endif // _WIN32
+
+#ifndef NO_CLASSIC
+ m_classic_ui.reset(new QJackTrip(*this));
+ QObject::connect(m_classic_ui.data(), &QJackTrip::signalExit, app,
+ &QCoreApplication::quit, Qt::QueuedConnection);
+#ifdef NO_VS
+ m_classic_ui->show();
+#endif // NO_VS
+#endif // NO_CLASSIC
+
+ QSettings settings;
+
+#ifndef NO_VS
+ m_vs_ui.reset(new VirtualStudio(*this));
+ QObject::connect(m_vs_ui.data(), &VirtualStudio::signalExit, app,
+ &QCoreApplication::quit, Qt::QueuedConnection);
+ // Check which mode we are running in
+ uiModeT uiMode = UserInterface::MODE_UNSET;
+ if (!m_cliSettings->getDeeplink().isEmpty()) {
+ uiMode = MODE_VS;
+ } else if (m_cliSettings->guiForceClassicMode()) {
+ uiMode = MODE_CLASSIC;
+ // force settings change; otherwise, virtual studio
+ // window will still be displayed
+ settings.setValue(QStringLiteral("UiMode"), uiMode);
+ } else {
+ uiMode = static_cast<uiModeT>(
+ settings.value(QStringLiteral("UiMode"), MODE_UNSET).toInt());
+ }
+ setMode(uiMode);
+#endif // NO_VS
+
+#if !defined(NO_UPDATER) && !defined(__unix__)
+#ifndef PSI
+ QString updateChannel =
+ settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower();
+ QString baseUrl =
+ QStringLiteral("https://files.jacktrip.org/app-releases/%1").arg(updateChannel);
+#else
+ QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip");
+#endif // PSI
+ // Setup auto-update feed
+ dblsqd::Feed* feed = new dblsqd::Feed();
+#ifdef _WIN32
+ feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "win")));
+#endif
+#ifdef Q_OS_MACOS
+ feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "mac")));
+#endif
+ if (feed) {
+ dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
+ updateDialog->setIcon(":/qjacktrip/icon.png");
+ }
+#endif // !defined(NO_UPDATER) && !defined(__unix__)
+}
+
+void UserInterface::setMode(uiModeT m)
+{
+#ifdef NO_VS
+ if (m == MODE_VS) {
+ std::cerr << "JackTrip was not built with support for Virtual Studio mode."
+ << std::endl;
+ }
+ m = MODE_CLASSIC;
+#endif
+
+#ifdef NO_CLASSIC
+ if (m == MODE_CLASSIC) {
+ std::cerr << "JackTrip was not built with support for Classic mode." << std::endl;
+ }
+ m = MODE_VS;
+#endif
+
+ switch (m) {
+ case MODE_UNSET:
+ case MODE_VS:
+#ifndef NO_VS
+ m_vs_ui->show();
+ if (m == MODE_VS || (m == MODE_UNSET && m_vs_ui->vsFtux())) {
+ m_vs_ui->setWindowState(QStringLiteral("login"));
+ } else if (m == MODE_UNSET && !m_vs_ui->vsFtux()) {
+ m_vs_ui->setWindowState(QStringLiteral("start"));
+ }
+ if (m_vs_ui->windowState() == "login")
+ m_vs_ui->login();
+#ifndef NO_CLASSIC
+ if (m_uiMode == MODE_CLASSIC)
+ m_classic_ui->hide();
+#endif // NO_CLASSIC
+#endif // NO_VS
+ m_uiMode = MODE_VS;
+ break;
+ case MODE_CLASSIC:
+#ifndef NO_CLASSIC
+ m_classic_ui->show();
+#ifndef NO_VS
+ m_vs_ui->hide();
+#endif // NO_VS
+#endif // NO_CLASSIC
+ m_uiMode = MODE_CLASSIC;
+ break;
+ default:
+ return;
+ }
+}
+
+void UserInterface::enableNap()
+{
+#ifdef __APPLE__
+ m_noNap.enableNap();
+#endif
+}
+
+void UserInterface::disableNap()
+{
+#ifdef __APPLE__
+ m_noNap.disableNap();
+#endif
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024 Michael Dickey, Aaron Wyatt.
+
+ 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 UserInterface.h
+ * \author Michael Dickey
+ * \date August 2024
+ */
+
+#ifndef __USERINTERFACE_H__
+#define __USERINTERFACE_H__
+
+#include <QApplication>
+#include <QMainWindow>
+#include <QSharedPointer>
+
+#include "Settings.h"
+
+#ifdef __APPLE__
+#include "NoNap.h"
+#endif
+
+// forward class declarations
+class VirtualStudio;
+class QJackTrip;
+
+/// UserInterface manages graphical user interfaces for JackTrip
+class UserInterface
+{
+ public:
+ /**
+ * @brief which GUI mode is in use
+ *
+ * MODE_UNSET: none selected yet
+ * MODE_VS: Virtual Studio mode (QML/Quick interface)
+ * MODE_CLASSIC: Classic mode (QJackTrip interface)
+ */
+ enum uiModeT { MODE_UNSET, MODE_VS, MODE_CLASSIC };
+
+ /// construction requires command line settings
+ explicit UserInterface(QSharedPointer<Settings>& settings);
+
+ /// @brief simple destructor
+ ~UserInterface();
+
+ /// @return current GUI mode
+ inline uiModeT getMode() const { return m_uiMode; }
+
+ /// @return command line settings
+ inline Settings& getSettings() { return *m_cliSettings.data(); }
+
+ /// @brief creates new application using command line arguments
+ static QCoreApplication* createApplication(int& argc, char* argv[]);
+
+ /// @brief starts graphical user interface
+ void start(QApplication* app);
+
+ /// @brief sets GUI mode
+ void setMode(uiModeT m);
+
+ /// @brief enables napping for OSX
+ void enableNap();
+
+ /// @brief disables napping for OSX
+ void disableNap();
+
+ private:
+ uiModeT m_uiMode = MODE_UNSET;
+ QSharedPointer<Settings> m_cliSettings;
+
+#ifndef NO_VS
+ QSharedPointer<VirtualStudio> m_vs_ui;
+#endif
+
+#ifndef NO_CLASSIC
+ QSharedPointer<QJackTrip> m_classic_ui;
+#endif
+
+#ifdef __APPLE__
+ NoNap m_noNap;
+#endif
+};
+
+#endif
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
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.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
JackTrip: A System for High-Quality Audio Network Performance
over the Internet
- Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
- SoundWIRE group at CCRMA, Stanford University.
- JackTrip Labs, Inc.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
JackTrip: A System for High-Quality Audio Network Performance
over the Internet
- Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
- SoundWIRE group at CCRMA, Stanford University.
- JackTrip Labs, Inc.
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- id: appIcon
-
- property alias icon: btn.icon
- property string color: ""
- property string defaultColor: virtualstudio.darkMode ? "#CCCCCC" : "#333333"
- signal clicked()
-
- Button {
- id: btn
- anchors.fill: parent
- anchors.centerIn: parent
- topInset: 0
- leftInset: 0
- rightInset: 0
- bottomInset: 0
- padding: 0
-
- background: Rectangle { color: "transparent" }
- icon.color: color ? color : defaultColor
- icon.width: parent.width
- icon.height: parent.height
- display: AbstractButton.IconOnly
- onClicked: appIcon.clicked()
- }
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 AudioInterfaceMode.h
- * \author Matt Horton
- * \date December 2022
- */
-
-#ifndef AUDIOINTERFACEMODE_H
-#define AUDIOINTERFACEMODE_H
-
-enum class AudioInterfaceMode {
- JACK, ///< Jack Mode
- RTAUDIO, ///< RtAudio Mode
- ALL,
- NONE
-};
-
-#ifdef RT_AUDIO
-#ifndef NO_JACK
-constexpr AudioInterfaceMode mode = AudioInterfaceMode::ALL;
-#else
-constexpr AudioInterfaceMode mode = AudioInterfaceMode::RTAUDIO;
-#endif
-#else
-#ifndef NO_JACK
-constexpr AudioInterfaceMode mode = AudioInterfaceMode::JACK;
-#else
-constexpr AudioInterfaceMode mode = AudioInterfaceMode::NONE;
-#endif
-#endif
-
-template<AudioInterfaceMode backend>
-constexpr auto isBackendAvailable()
-{
- if constexpr (backend == AudioInterfaceMode::RTAUDIO) {
- if (mode == AudioInterfaceMode::RTAUDIO || mode == AudioInterfaceMode::ALL) {
- return true;
- } else {
- return false;
- }
- } else if constexpr (backend == AudioInterfaceMode::JACK) {
- if (mode == AudioInterfaceMode::JACK || mode == AudioInterfaceMode::ALL) {
- return true;
- } else {
- return false;
- }
- } else if constexpr (backend == AudioInterfaceMode::ALL) {
- if (mode == AudioInterfaceMode::ALL) {
- return true;
- } else {
- return false;
- }
- } else {
- return false;
- }
-}
-
-#endif // AUDIOINTERFACEMODE_H
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Rectangle {
- width: parent.width
- height: parent.height
- color: backgroundColour
-
- property bool connected: false
- property bool showMeters: true
- property bool showTestAudio: true
-
- property int fontBig: 20
- property int fontMedium: 13
- property int fontSmall: 11
- property int fontExtraSmall: 8
-
- property int leftMargin: 48
- property int rightMargin: 16
- property int bottomToolTipMargin: 8
- property int rightToolTipMargin: 4
- property int buttonWidth: 103
- property int buttonHeight: 25
-
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
- property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
- property string toolTipTextColour: textColour
-
- property string errorFlagColour: "#DB0A0A"
- property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-
- function getCurrentInputDeviceIndex () {
- if (audio.inputDevice === "") {
- return audio.inputComboModel.findIndex(elem => elem.type === "element");
- }
-
- let idx = audio.inputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.inputDevice);
- if (idx < 0) {
- idx = audio.inputComboModel.findIndex(elem => elem.type === "element");
- }
- return idx;
- }
-
- function getCurrentOutputDeviceIndex() {
- if (audio.outputDevice === "") {
- return audio.outputComboModel.findIndex(elem => elem.type === "element");
- }
-
- let idx = audio.outputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.outputDevice);
- if (idx < 0) {
- idx = audio.outputComboModel.findIndex(elem => elem.type === "element");
- }
- return idx;
- }
-
- function getCurrentInputChannelsIndex() {
- let idx = audio.inputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseInputChannel
- && elem.numChannels === audio.numInputChannels);
- if (idx < 0) {
- idx = 0;
- }
- return idx;
- }
-
- function getCurrentOutputChannelsIndex() {
- let idx = audio.outputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseOutputChannel
- && elem.numChannels === audio.numOutputChannels);
- if (idx < 0) {
- idx = 0;
- }
- return idx;
- }
-
- function getCurrentMixModeIndex() {
- let idx = audio.inputMixModeComboModel.findIndex(elem => elem.value === audio.inputMixMode);
- if (idx < 0) {
- idx = 0;
- }
- return idx;
- }
-
- Loader {
- anchors.fill: parent
- sourceComponent: audio.audioBackend == "JACK" ? usingJACK : ((!audio.deviceModelsInitialized || audio.scanningDevices) ? scanningDevices : usingRtAudio);
- }
-
- Component {
- id: usingRtAudio
-
- Item {
- anchors.top: parent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.leftMargin: 24 * virtualstudio.uiScale
- anchors.right: parent.right
-
- Rectangle {
- id: leftSpacer
- x: 0; y: 0
- width: 144 * virtualstudio.uiScale
- height: 0
- color: "transparent"
- }
-
- Text {
- id: outputLabel
- x: 0; y: 0
- text: "Output Device"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- bottomPadding: 10 * virtualstudio.uiScale
- color: textColour
- }
-
- InfoTooltip {
- id: outputHelpIcon
- anchors.left: outputLabel.right
- anchors.bottom: outputLabel.top
- anchors.bottomMargin: -8 * virtualstudio.uiScale
- size: 16 * virtualstudio.uiScale
- content: qsTr("How you'll hear the studio audio")
- }
-
- AppIcon {
- id: headphonesIcon
- anchors.left: outputLabel.left
- anchors.top: outputLabel.bottom
- width: 28 * virtualstudio.uiScale
- height: 28 * virtualstudio.uiScale
- icon.source: "headphones.svg"
- }
-
- ComboBox {
- id: outputCombo
- anchors.left: leftSpacer.right
- anchors.verticalCenter: outputLabel.verticalCenter
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale
- model: audio.outputComboModel
- currentIndex: getCurrentOutputDeviceIndex()
- delegate: ItemDelegate {
- required property var modelData
- required property int index
-
- leftPadding: 0
-
- width: parent.width
- contentItem: Text {
- leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
- text: modelData.text
- font.bold: modelData.type === "header"
- }
- highlighted: outputCombo.highlightedIndex === index
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (modelData.type == "element") {
- outputCombo.currentIndex = index
- outputCombo.popup.close()
- audio.outputDevice = modelData.text
- if (modelData.category.startsWith("Low-Latency")) {
- let inputComboIdx = inputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
- if (inputComboIdx !== null && inputComboIdx !== undefined) {
- inputCombo.currentIndex = inputComboIdx;
- audio.inputDevice = modelData.text
- }
- }
- if (connected) {
- virtualstudio.triggerReconnect(false);
- } else {
- audio.validateDevices()
- audio.restartAudio()
- }
- }
- }
- }
- }
- contentItem: Text {
- leftPadding: 12
- font: outputCombo.font
- horizontalAlignment: Text.AlignHLeft
- verticalAlignment: Text.AlignVCenter
- elide: Text.ElideRight
- text: outputCombo.model[outputCombo.currentIndex]!=undefined && outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : ""
- }
- }
-
- Meter {
- id: outputDeviceMeters
- anchors.left: outputCombo.left
- anchors.right: outputCombo.right
- anchors.top: outputCombo.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- model: showMeters ? audio.outputMeterLevels : [0, 0]
- clipped: audio.outputClipped
- visible: showMeters
- enabled: audio.audioReady && !Boolean(audio.devicesError)
- }
-
- VolumeSlider {
- id: outputSlider
- anchors.left: outputCombo.left
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.top: outputDeviceMeters.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 30 * virtualstudio.uiScale
- labelText: "Studio"
- tooltipText: "How loudly you hear other participants"
- showLabel: false
- sliderEnabled: true
- visible: showMeters
- }
-
- Text {
- id: outputChannelsLabel
- anchors.left: outputCombo.left
- anchors.right: outputCombo.horizontalCenter
- anchors.top: showMeters ? outputSlider.bottom : outputCombo.bottom
- anchors.topMargin: 12 * virtualstudio.uiScale
- textFormat: Text.RichText
- text: "Output Channel(s)"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: outputChannelsCombo
- anchors.left: outputCombo.left
- anchors.right: outputCombo.horizontalCenter
- anchors.rightMargin: 8 * virtualstudio.uiScale
- anchors.top: outputChannelsLabel.bottom
- anchors.topMargin: 4 * virtualstudio.uiScale
- model: audio.outputChannelsComboModel
- enabled: audio.outputChannelsComboModel.length > 1
- currentIndex: getCurrentOutputChannelsIndex()
- delegate: ItemDelegate {
- required property var modelData
- required property int index
- width: parent.width
- contentItem: Text {
- text: modelData.label
- }
- highlighted: outputChannelsCombo.highlightedIndex === index
- MouseArea {
- anchors.fill: parent
- onClicked: {
- outputChannelsCombo.currentIndex = index
- outputChannelsCombo.popup.close()
- audio.baseOutputChannel = modelData.baseChannel
- audio.numOutputChannels = modelData.numChannels
- if (connected) {
- virtualstudio.triggerReconnect(false);
- } else {
- audio.validateDevices()
- audio.restartAudio()
- }
- }
- }
- }
- contentItem: Text {
- leftPadding: 12
- font: inputCombo.font
- horizontalAlignment: Text.AlignHLeft
- verticalAlignment: Text.AlignVCenter
- elide: Text.ElideRight
- text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || ""
- }
- }
-
- Button {
- id: testOutputAudioButton
- visible: showTestAudio
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { audio.playOutputAudio() }
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: outputChannelsCombo.verticalCenter
- width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Play Test Tone"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Rectangle {
- id: divider1
- anchors.top: showTestAudio ? testOutputAudioButton.bottom : outputChannelsCombo.bottom
- anchors.topMargin: 24 * virtualstudio.uiScale
- width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale
- color: "#E0E0E0"
- }
-
- Text {
- id: inputLabel
- anchors.left: outputLabel.left
- anchors.top: divider1.bottom
- anchors.topMargin: 24 * virtualstudio.uiScale
- text: "Input Device"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- bottomPadding: 10 * virtualstudio.uiScale
- color: textColour
- }
-
- InfoTooltip {
- id: inputHelpIcon
- anchors.left: inputLabel.right
- anchors.bottom: inputLabel.top
- anchors.bottomMargin: -8 * virtualstudio.uiScale
- size: 16 * virtualstudio.uiScale
- content: qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)")
- }
-
- AppIcon {
- id: microphoneIcon
- anchors.left: inputLabel.left
- anchors.top: inputLabel.bottom
- width: 32 * virtualstudio.uiScale
- height: 32 * virtualstudio.uiScale
- icon.source: "mic.svg"
- }
-
- ComboBox {
- id: inputCombo
- model: audio.inputComboModel
- currentIndex: getCurrentInputDeviceIndex()
- anchors.left: outputCombo.left
- anchors.right: outputCombo.right
- anchors.verticalCenter: inputLabel.verticalCenter
- delegate: ItemDelegate {
- required property var modelData
- required property int index
-
- leftPadding: 0
-
- width: parent.width
- contentItem: Text {
- leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
- text: modelData.text
- font.bold: modelData.type === "header"
- }
- highlighted: inputCombo.highlightedIndex === index
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (modelData.type == "element") {
- inputCombo.currentIndex = index
- inputCombo.popup.close()
- audio.inputDevice = modelData.text
- if (modelData.category.startsWith("Low-Latency")) {
- let outputComboIdx = outputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
- if (outputComboIdx !== null && outputComboIdx !== undefined) {
- outputCombo.currentIndex = outputComboIdx;
- audio.outputDevice = modelData.text
- }
- }
- if (connected) {
- virtualstudio.triggerReconnect(false);
- } else {
- audio.validateDevices()
- audio.restartAudio()
- }
- }
- }
- }
- }
- contentItem: Text {
- leftPadding: 12
- font: inputCombo.font
- horizontalAlignment: Text.AlignHLeft
- verticalAlignment: Text.AlignVCenter
- elide: Text.ElideRight
- text: inputCombo.model[inputCombo.currentIndex] != undefined && inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : ""
- }
- }
-
- Meter {
- id: inputDeviceMeters
- anchors.left: inputCombo.left
- anchors.right: inputCombo.right
- anchors.top: inputCombo.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- model: showMeters ? audio.inputMeterLevels : [0, 0]
- clipped: audio.inputClipped
- visible: showMeters
- enabled: audio.audioReady && !Boolean(audio.devicesError)
- }
-
- VolumeSlider {
- id: inputSlider
- anchors.left: inputCombo.left
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.top: inputDeviceMeters.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 30 * virtualstudio.uiScale
- labelText: "Send"
- tooltipText: "How loudly other participants hear you"
- showLabel: false
- sliderEnabled: true
- visible: showMeters
- }
-
- Button {
- id: hiddenInputButton
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: inputSlider.verticalCenter
- width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- visible: false
- }
-
- Text {
- id: inputChannelsLabel
- anchors.left: inputCombo.left
- anchors.right: inputCombo.horizontalCenter
- anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
- anchors.topMargin: 12 * virtualstudio.uiScale
- textFormat: Text.RichText
- text: "Input Channel(s)"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: inputChannelsCombo
- anchors.left: inputCombo.left
- anchors.right: inputCombo.horizontalCenter
- anchors.rightMargin: 8 * virtualstudio.uiScale
- anchors.top: inputChannelsLabel.bottom
- anchors.topMargin: 4 * virtualstudio.uiScale
- model: audio.inputChannelsComboModel
- enabled: audio.inputChannelsComboModel.length > 1
- currentIndex: getCurrentInputChannelsIndex()
- delegate: ItemDelegate {
- required property var modelData
- required property int index
- width: parent.width
- contentItem: Text {
- text: modelData.label
- }
- highlighted: inputChannelsCombo.highlightedIndex === index
- MouseArea {
- anchors.fill: parent
- onClicked: {
- inputChannelsCombo.currentIndex = index
- inputChannelsCombo.popup.close()
- audio.baseInputChannel = modelData.baseChannel
- audio.numInputChannels = modelData.numChannels
- if (connected) {
- virtualstudio.triggerReconnect(false);
- } else {
- audio.validateDevices()
- audio.restartAudio()
- }
- }
- }
- }
- contentItem: Text {
- leftPadding: 12
- font: inputCombo.font
- horizontalAlignment: Text.AlignHLeft
- verticalAlignment: Text.AlignVCenter
- elide: Text.ElideRight
- text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || ""
- }
- }
-
- Text {
- id: inputMixModeLabel
- anchors.left: inputCombo.horizontalCenter
- anchors.right: inputCombo.right
- anchors.rightMargin: 8 * virtualstudio.uiScale
- anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
- anchors.topMargin: 12 * virtualstudio.uiScale
- textFormat: Text.RichText
- text: "Mono / Stereo"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: inputMixModeCombo
- anchors.left: inputCombo.horizontalCenter
- anchors.right: inputCombo.right
- anchors.rightMargin: 8 * virtualstudio.uiScale
- anchors.top: inputMixModeLabel.bottom
- anchors.topMargin: 4 * virtualstudio.uiScale
- model: audio.inputMixModeComboModel
- enabled: audio.inputMixModeComboModel.length > 1
- currentIndex: getCurrentMixModeIndex()
- delegate: ItemDelegate {
- required property var modelData
- required property int index
- width: parent.width
- contentItem: Text {
- text: modelData.label
- }
- highlighted: inputMixModeCombo.highlightedIndex === index
- MouseArea {
- anchors.fill: parent
- onClicked: {
- inputMixModeCombo.currentIndex = index
- inputMixModeCombo.popup.close()
- audio.inputMixMode = audio.inputMixModeComboModel[index].value
- if (connected) {
- virtualstudio.triggerReconnect(false);
- } else {
- audio.validateDevices()
- audio.restartAudio()
- }
- }
- }
- }
- contentItem: Text {
- leftPadding: 12
- font: inputCombo.font
- horizontalAlignment: Text.AlignHLeft
- verticalAlignment: Text.AlignVCenter
- elide: Text.ElideRight
- text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || ""
- }
- }
-
- Text {
- id: inputChannelHelpMessage
- anchors.left: inputChannelsCombo.left
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.right: inputChannelsCombo.right
- anchors.top: inputChannelsCombo.bottom
- anchors.topMargin: 8 * virtualstudio.uiScale
- textFormat: Text.RichText
- wrapMode: Text.WordWrap
- text: audio.inputChannelsComboModel.length > 1 ? "Choose up to 2 channels" : "Only 1 channel available"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Text {
- id: inputMixModeHelpMessage
- anchors.left: inputMixModeCombo.left
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.right: inputMixModeCombo.right
- anchors.top: inputMixModeCombo.bottom
- anchors.topMargin: 8 * virtualstudio.uiScale
- textFormat: Text.RichText
- wrapMode: Text.WordWrap
- text: (() => {
- if (audio.inputMixMode === 2) {
- return "Treat the channels as Left and Right signals, coming through each speaker separately.";
- } else if (audio.inputMixMode === 3) {
- return "Combine the channels into one central channel coming through both speakers.";
- } else if (audio.inputMixMode === 1) {
- return "Send a single channel of audio";
- } else {
- return "";
- }
- })()
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Connections {
- target: audio
- // anything that sets currentIndex to the value of a function needs
- // to be manually updated whenever there is a change to any vars it uses
- function onInputDeviceChanged() {
- inputCombo.currentIndex = getCurrentInputDeviceIndex();
- }
- function onOutputDeviceChanged() {
- outputCombo.currentIndex = getCurrentOutputDeviceIndex();
- }
- function onNumInputChannelsChanged() {
- inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
- }
- function onBaseInputChannelChanged() {
- inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
- }
- function onNumOutputChannelsChanged() {
- outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
- }
- function onBaseOutputChannelChanged() {
- outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
- }
- function onInputMixModeChanged() {
- inputMixModeCombo.currentIndex = getCurrentMixModeIndex();
- }
- }
- }
- }
-
- Component {
- id: usingJACK
-
- Item {
- anchors.top: parent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.leftMargin: leftMargin * virtualstudio.uiScale
- anchors.right: parent.right
-
- Text {
- id: jackLabel
- x: 0; y: 0
- width: parent.width - rightMargin * virtualstudio.uiScale
- text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- wrapMode: Text.WordWrap
- color: textColour
- }
-
- Text {
- id: jackOutputLabel
- anchors.left: jackLabel.left
- anchors.top: jackLabel.bottom
- anchors.topMargin: 48 * virtualstudio.uiScale
- width: 144 * virtualstudio.uiScale
- text: "Output Volume"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- wrapMode: Text.WordWrap
- bottomPadding: 10 * virtualstudio.uiScale
- color: textColour
- }
-
- AppIcon {
- id: jackHeadphonesIcon
- anchors.left: jackOutputLabel.left
- anchors.top: jackOutputLabel.bottom
- width: 28 * virtualstudio.uiScale
- height: 28 * virtualstudio.uiScale
- icon.source: "headphones.svg"
- }
-
- Meter {
- id: jackOutputMeters
- anchors.left: jackOutputLabel.right
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: jackOutputLabel.verticalCenter
- height: 24 * virtualstudio.uiScale
- model: showMeters ? audio.outputMeterLevels : [0, 0]
- clipped: audio.outputClipped
- enabled: audio.audioReady && !Boolean(audio.devicesError)
- }
-
- Button {
- id: jackTestOutputAudioButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { audio.playOutputAudio() }
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter
- width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Play Test Tone"
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- VolumeSlider {
- id: jackOutputVolumeSlider
- anchors.left: jackOutputMeters.left
- anchors.right: jackTestOutputAudioButton.left
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.top: jackOutputMeters.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 30 * virtualstudio.uiScale
- labelText: "Studio"
- tooltipText: "How loudly you hear other participants"
- showLabel: false
- sliderEnabled: true
- }
-
- Text {
- id: jackInputLabel
- anchors.left: jackLabel.left
- anchors.top: jackOutputVolumeSlider.bottom
- anchors.topMargin: 48 * virtualstudio.uiScale
- width: 144 * virtualstudio.uiScale
- text: "Input Volume"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- wrapMode: Text.WordWrap
- bottomPadding: 10 * virtualstudio.uiScale
- color: textColour
- }
-
- AppIcon {
- id: jackMicrophoneIcon
- anchors.left: jackInputLabel.left
- anchors.top: jackInputLabel.bottom
- width: 32 * virtualstudio.uiScale
- height: 32 * virtualstudio.uiScale
- icon.source: "mic.svg"
- }
-
- Meter {
- id: jackInputMeters
- anchors.left: jackInputLabel.right
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: jackInputLabel.verticalCenter
- height: 24 * virtualstudio.uiScale
- model: showMeters ? audio.inputMeterLevels : [0, 0]
- clipped: audio.inputClipped
- enabled: audio.audioReady && !Boolean(audio.devicesError)
- }
-
- VolumeSlider {
- id: jackInputVolumeSlider
- anchors.left: jackInputMeters.left
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.top: jackInputMeters.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- height: 30 * virtualstudio.uiScale
- labelText: "Send"
- tooltipText: "How loudly other participants hear you"
- showLabel: false
- sliderEnabled: true
- }
-
- Button {
- id: jackHiddenInputButton
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: jackInputVolumeSlider.verticalCenter
- width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- visible: false
- }
- }
- }
-
- Component {
- id: noBackend
-
- Item {
- anchors.top: parent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.leftMargin: leftMargin * virtualstudio.uiScale
- anchors.right: parent.right
-
- Text {
- id: noBackendLabel
- x: 0; y: 0
- width: parent.width - (16 * virtualstudio.uiScale)
- text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag."
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- wrapMode: Text.WordWrap
- color: textColour
- }
- }
- }
-
- Component {
- id: scanningDevices
-
- Item {
- anchors.top: parent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.leftMargin: leftMargin * virtualstudio.uiScale
- anchors.right: parent.right
-
- Text {
- id: scanningDevicesLabel
- x: 0; y: 0
- width: parent.width - (16 * virtualstudio.uiScale)
- text: "Scanning audio devices..."
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- wrapMode: Text.WordWrap
- color: textColour
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- Rectangle {
- width: parent.width; height: parent.height
- color: backgroundColour
- }
-
- property bool refreshing: false
-
- property int buttonHeight: 25
- property int buttonWidth: 103
- property int extraSettingsButtonWidth: 16
- property int emptyListMessageWidth: 450
- property int createMessageTopMargin: 16
- property int createButtonTopMargin: 24
- property int fontBig: 28
- property int fontMedium: 11
-
- property int scrollY: 0
-
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D"
-
- function refresh() {
- scrollY = studioListView.contentY;
- var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY);
- if (currentIndex == -1) {
- currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
- }
- virtualstudio.refreshStudios(currentIndex, true)
- }
-
- Rectangle {
- z: 1
- width: parent.width; height: parent.height
- color: "#40000000"
- visible: refreshing
- MouseArea {
- anchors.fill: parent
- propagateComposedEvents: false
- hoverEnabled: true
- preventStealing: true
- }
- }
-
- Component {
- id: footer
- Rectangle {
- height: 16 * virtualstudio.uiScale
- x: 16 * virtualstudio.uiScale
- width: parent.width - (2 * x)
- color: backgroundColour
- }
- }
-
- ListView {
- id: studioListView
- x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale
- spacing: 16 * virtualstudio.uiScale
- header: footer
- footer: footer
- model: virtualstudio.serverModel
- clip: true
- boundsBehavior: Flickable.StopAtBounds
- delegate: Studio {
- x: 16 * virtualstudio.uiScale
- width: studioListView.width - (2 * x)
- serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : ""
- flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag
- studioName: modelData.name
- publicStudio: modelData.isPublic
- admin: modelData.isAdmin
- available: modelData.canConnect
- connected: false
- studioId: modelData.id ? modelData.id : ""
- streamId: modelData.streamId ? modelData.streamId : ""
- inviteKeyString: modelData.inviteKey ? modelData.inviteKey : ""
- sampleRate: modelData.sampleRate
- }
-
- section { property: "modelData.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
- }
-
- Button {
- id: resetFiltersButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- visible: parent.count == 0 && !virtualstudio.showCreateStudio
- onClicked: {
- virtualstudio.showSelfHosted = false;
- virtualstudio.showInactive = true;
- refreshing = true;
- refresh();
- }
- anchors.top: emptyListMessage.bottom
- anchors.topMargin: createButtonTopMargin
- anchors.horizontalCenter: emptyListMessage.horizontalCenter
- width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
- Text {
- text: "Reset Filters"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- 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: "Looks like you're not a member of any studios!\nHave the studio owner send you an invite link, or create your own studio to invite others."
- 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" : createButtonStroke
- layer.enabled: createButton.hovered && !createButton.down
- }
- 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 {
- z: -1
- anchors.fill: parent
- onWheel: function (wheel) {
- // trackpad
- studioListView.contentY -= wheel.pixelDelta.y;
- // mouse wheel
- studioListView.contentY -= wheel.angleDelta.y;
- studioListView.returnToBounds();
- }
- }
-
- Component.onCompleted: {
- // Customize scroll properties on different platforms
- if (Qt.platform.os == "linux" || Qt.platform.os == "osx" ||
- Qt.platform.os == "unix" || Qt.platform.os == "windows") {
- var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}',
- studioListView,
- "dynamicSnippet1");
- scrollBar.policy = ScrollBar.AlwaysOn;
- ScrollBar.vertical = scrollBar;
- }
- }
- }
-
- Rectangle {
- x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale
- border.color: "#33979797"
- color: backgroundColour
-
- Button {
- id: refreshButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { refreshing = true; refresh() }
- anchors.verticalCenter: parent.verticalCenter
- x: 16 * virtualstudio.uiScale
- width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
- Text {
- text: "Refresh List"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Button {
- id: aboutButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { virtualstudio.showAbout() }
- anchors.verticalCenter: parent.verticalCenter
- x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale)
- width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
- Text {
- text: "About"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Button {
- id: settingsButton
- text: "Settings"
- palette.buttonText: textColour
- icon {
- source: "cog.svg";
- color: textColour;
- }
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); }
- display: AbstractButton.TextBesideIcon
- font {
- family: "Poppins";
- pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale;
- }
- leftPadding: 0
- rightPadding: 4
- spacing: 0
- anchors.verticalCenter: parent.verticalCenter
- x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale)
- width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
- }
- }
-
- FeedbackSurvey {
- }
-
- Connections {
- target: virtualstudio
- // Need to do this to avoid layout issues with our section header.
- function onNewScale() {
- studioListView.positionViewAtEnd();
- studioListView.positionViewAtBeginning();
- scrollY = studioListView.contentY;
- }
- function onRefreshFinished(index) {
- refreshing = false;
- if (index == -1) {
- studioListView.contentY = scrollY
- } else {
- studioListView.positionViewAtIndex(index, ListView.Beginning);
- }
- }
- function onPeriodicRefresh() { refresh() }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Rectangle {
- width: parent.width; height: parent.height
- color: backgroundColour
- clip: true
-
- property int fontBig: 28
- property int fontMedium: 12
- property int fontSmall: 10
- property int fontTiny: 8
-
- property int rightMargin: 16
- property int bottomToolTipMargin: 8
- property int rightToolTipMargin: 4
-
- property string saveButtonText: "#DB0A0A"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
- property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
- property real muteButtonMutedLightnessValue: 0.24
- property real muteButtonMutedSaturationValue: 0.73
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
- property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
- property string toolTipTextColour: textColour
-
- property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
- property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-
- property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
-
- MouseArea {
- anchors.fill: parent
- propagateComposedEvents: false
- }
-
- Rectangle {
- id: audioSettingsView
- width: parent.width;
- height: parent.height;
- color: backgroundColour
- radius: 6 * virtualstudio.uiScale
-
- DeviceRefreshButton {
- id: refreshButton
- anchors.top: parent.top;
- anchors.topMargin: 16 * virtualstudio.uiScale;
- anchors.right: parent.right;
- anchors.rightMargin: 16 * virtualstudio.uiScale;
- enabled: !audio.scanningDevices
- onDeviceRefresh: function () {
- virtualstudio.triggerReconnect(true);
- }
- }
-
- Text {
- text: "Restarting Audio"
- anchors.verticalCenter: refreshButton.verticalCenter
- anchors.right: refreshButton.left;
- anchors.rightMargin: 16 * virtualstudio.uiScale;
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- visible: audio.scanningDevices
- }
-
- AudioSettings {
- id: audioSettings
- showMeters: false
- showTestAudio: false
- connected: true
- height: 300 * virtualstudio.uiScale
- anchors.top: refreshButton.bottom;
- anchors.topMargin: 16 * virtualstudio.uiScale;
- }
- }
-
- Button {
- id: backButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
- }
- onClicked: {
- virtualstudio.saveSettings();
- virtualstudio.windowState = "connected";
- }
- anchors.bottom: parent.bottom
- anchors.bottomMargin: 16 * virtualstudio.uiScale;
- anchors.left: parent.left
- anchors.leftMargin: 16 * virtualstudio.uiScale;
- width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale
-
- Text {
- text: "Back"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- DeviceWarning {
- id: deviceWarning
- anchors.left: backButton.right
- anchors.leftMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.bottomMargin: 16 * virtualstudio.uiScale;
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-import org.jacktrip.jacktrip 1.0
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property bool connecting: false
-
- property int leftHeaderMargin: 16
- property int fontBig: 28
- property int fontMedium: 12
- property int fontSmall: 10
- property int fontTiny: 8
-
- property int bodyMargin: 60
- property int rightMargin: 16
- property int bottomToolTipMargin: 8
- property int rightToolTipMargin: 4
-
- property string studioStatus: (virtualstudio.currentStudio.id === "" ? "" : virtualstudio.currentStudio.status)
- property bool showReadyScreen: studioStatus === "Ready"
- property bool showStartingScreen: studioStatus === "Starting"
- property bool showStoppingScreen: (virtualstudio.currentStudio.id === "" ? false : (virtualstudio.currentStudio.isAdmin && !virtualstudio.currentStudio.enabled && virtualstudio.currentStudio.cloudId !== ""))
- property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen
-
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-
- property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
- property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string saveButtonBackgroundColour: "#F2F3F3"
- property string saveButtonPressedColour: "#E7E8E8"
- property string saveButtonStroke: "#EAEBEB"
- property string saveButtonPressedStroke: "#B0B5B5"
- property string saveButtonText: "#DB0A0A"
-
- property string muteButtonMutedColor: "#FCB6B6"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
- property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
- property real muteButtonMutedLightnessValue: 0.24
- property real muteButtonMutedSaturationValue: 0.73
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
- property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
- property string toolTipTextColour: textColour
- property string warningTextColour: "#DB0A0A"
- property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
-
- property string meterGreen: "#61C554"
- property string meterYellow: "#F5BF4F"
- property string meterRed: "#F21B1B"
-
- property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
-
- Loader {
- id: studioWebLoader
- anchors.top: parent.top
- anchors.right: parent.right
- anchors.left: parent.left
- anchors.bottom: deviceControlsGroup.top
-
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
- property string studioId: virtualstudio.currentStudio.id
-
- source: accessToken && studioId ? "Web.qml" : "WebNull.qml"
- }
-
- DeviceControlsGroup {
- id: deviceControlsGroup
- anchors.bottom: footer.top
- }
-
- Footer {
- id: footer
- anchors.bottom: parent.bottom
- }
-}
+++ /dev/null
-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
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-import Qt5Compat.GraphicalEffects
-
-Item {
- width: parent.width
- height: parent.height
- clip: true
-
- required property bool isInput
- property string muteButtonMutedColor: "#FCB6B6"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#D5D9D9"
-
- Component {
- id: controlIndicator
-
- Button {
- id: iconButton
- width: 24 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
-
- anchors.left: parent.left
- anchors.leftMargin: 8 * virtualstudio.uiScale
-
- background: Rectangle {
- color: isInput ? (audio.inputMuted ? muteButtonMutedColor : buttonColour) : "transparent"
- width: 24 * virtualstudio.uiScale
- radius: 4 * virtualstudio.uiScale
- }
-
- onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
-
- AppIcon {
- id: iconImage
- anchors.centerIn: parent
- width: 24 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- icon.source: isInput ? (audio.inputMuted ? "micoff.svg" : "mic.svg") : "headphones.svg"
- color: isInput ? (audio.inputMuted ? "red" : ( virtualstudio.darkMode ? "#CCCCCC" : "#333333" )) : (virtualstudio.darkMode ? "#CCCCCC" : "#333333")
- onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
- }
-
- ToolTip {
- visible: isInput && iconButton.hovered
- x: iconButton.x + iconButton.width
- y: iconButton.y + iconButton.height
-
- contentItem: Text {
- text: audio.inputMuted ? qsTr("Click to unmute yourself") : qsTr("Click to mute yourself")
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- background: Rectangle {
- color: toolTipBackgroundColour
- radius: 4
- layer.enabled: true
- layer.effect: Glow {
- color: "#66000000"
- transparentBorder: true
- }
- }
- }
- }
- }
-
- Component {
- id: inputControls
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 2 * virtualstudio.uiScale
-
- VolumeSlider {
- Layout.fillWidth: true
- Layout.fillHeight: true
- labelText: "Send"
- tooltipText: "How loudly other participants hear you"
- sliderEnabled: !audio.inputMuted
- }
-
- DeviceWarning {
- id: deviceWarning
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
- }
- }
-
- Component {
- id: outputControls
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 4 * virtualstudio.uiScale
-
- VolumeSlider {
- Layout.fillWidth: true
- Layout.fillHeight: true
- labelText: "Studio"
- tooltipText: "How loudly you hear other participants"
- sliderEnabled: true
- }
-
- VolumeSlider {
- Layout.fillWidth: true
- Layout.fillHeight: true
- labelText: "Monitor"
- tooltipText: "How loudly you hear yourself"
- sliderEnabled: true
- }
- }
- }
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 5 * virtualstudio.uiScale
-
- Item {
- Layout.topMargin: 5 * virtualstudio.uiScale
- Layout.preferredHeight: 30 * virtualstudio.uiScale
- Layout.fillWidth: true
-
- RowLayout {
- anchors.fill: parent
- spacing: 8 * virtualstudio.uiScale
-
- Item {
- Layout.fillHeight: true
- Layout.preferredWidth: 100 * virtualstudio.uiScale
-
- Loader {
- id: typeIconIndicator
- anchors.left: parent.left
- sourceComponent: controlIndicator
- }
-
- Text {
- id: label
- anchors.left: parent.left
- anchors.leftMargin: 36 * virtualstudio.uiScale
-
- text: isInput ? "Input" : "Output"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- InfoTooltip {
- content: isInput ? qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") : qsTr("How you'll hear the studio audio")
- size: 16
- anchors.left: label.right
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.verticalCenter: label.verticalCenter
- }
- }
-
- Item {
- Layout.fillHeight: true
- Layout.fillWidth: true
- Layout.preferredWidth: 200 * virtualstudio.uiScale
-
- Meter {
- anchors.fill: parent
- anchors.rightMargin: 8 * virtualstudio.uiScale
- model: isInput ? audio.inputMeterLevels : audio.outputMeterLevels
- clipped: isInput ? audio.inputClipped : audio.outputClipped
- enabled: true
- }
- }
- }
- }
-
- Item {
- Layout.fillWidth: true
- Layout.fillHeight: true
- Layout.bottomMargin: 5 * virtualstudio.uiScale
-
- RowLayout {
- anchors.fill: parent
- spacing: 8 * virtualstudio.uiScale
-
- Item {
- Layout.fillHeight: true
- Layout.fillWidth: true
- Layout.leftMargin: 8 * virtualstudio.uiScale
- Layout.rightMargin: 8 * virtualstudio.uiScale
-
- Loader {
- anchors.fill: parent
- anchors.top: parent.top
- sourceComponent: isInput ? inputControls : outputControls
- }
- }
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-Rectangle {
- property string disabledButtonText: "#D3D4D4"
- property string saveButtonText: "#DB0A0A"
- property int fullHeight: 88 * virtualstudio.uiScale
- property int minimumHeight: 48 * virtualstudio.uiScale
-
- property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
- property bool isReady: virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
- property bool showDeviceControls: getShowDeviceControls()
-
- id: deviceControlsGroup
- width: parent.width
- height: isReady ? (showDeviceControls ? fullHeight : (feedbackDetectedModal.visible ? minimumHeight : 0)) : minimumHeight;
- color: backgroundColour
-
- function getShowDeviceControls () {
- // self-managed servers do not support minified controls so keep it full size
- return (!virtualstudio.currentStudio.isManaged && virtualstudio.currentStudio.sessionId === "") || (!virtualstudio.collapseDeviceControls && isReady);
- }
-
- MouseArea {
- anchors.fill: parent
- propagateComposedEvents: false
- }
-
- RowLayout {
- id: layout
- anchors.fill: parent
- spacing: 2
- visible: !feedbackDetectedModal.visible
-
- Item {
- Layout.fillHeight: true
- Layout.fillWidth: true
- visible: !isReady
-
- 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.disconnect()
-
- Text {
- text: "Back to Studios"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
- }
-
- Item {
- Layout.fillHeight: true
- Layout.fillWidth: true
- visible: showDeviceControls
-
- DeviceControls {
- isInput: true
- }
- }
-
- Item {
- Layout.fillHeight: true
- Layout.fillWidth: true
- visible: showDeviceControls
-
- DeviceControls {
- isInput: false
- }
- }
-
- Item {
- Layout.fillHeight: true
- Layout.preferredWidth: 48 * virtualstudio.uiScale
- visible: showDeviceControls
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 2
-
- Item {
- Layout.preferredHeight: 24 * virtualstudio.uiScale
- Layout.preferredWidth: 24 * virtualstudio.uiScale
- Layout.topMargin: 2 * virtualstudio.uiScale
- Layout.rightMargin: 2 * virtualstudio.uiScale
- Layout.alignment: Qt.AlignRight | Qt.AlignTop
-
- Button {
- id: closeDeviceControlsButton
- visible: virtualstudio.currentStudio.isManaged || virtualstudio.currentStudio.sessionId !== ""
- width: 24 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- background: Rectangle {
- color: backgroundColour
- }
- anchors.top: parent.top
- anchors.right: parent.right
- onClicked: {
- virtualstudio.collapseDeviceControls = true;
- }
-
- AppIcon {
- id: closeDeviceControlsIcon
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- width: 24 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- color: closeDeviceControlsButton.hovered ? textColour : browserButtonHoverColour
- icon.source: "close.svg"
- onClicked: {
- virtualstudio.collapseDeviceControls = true;
- }
- }
- }
- }
-
- Item {
- visible: isUsingRtAudio
- Layout.preferredWidth: 40 * virtualstudio.uiScale
- Layout.preferredHeight: 64 * virtualstudio.uiScale
- Layout.bottomMargin: 5 * virtualstudio.uiScale
- Layout.topMargin: 2 * virtualstudio.uiScale
- Layout.rightMargin: 2 * virtualstudio.uiScale
- Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
-
- Button {
- id: changeDevicesButton
- width: 36 * virtualstudio.uiScale
- height: 36 * virtualstudio.uiScale
- anchors.top: parent.top
- anchors.horizontalCenter: parent.horizontalCenter
- background: Rectangle {
- radius: 8 * virtualstudio.uiScale
- color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour)
- }
- onClicked: {
- virtualstudio.windowState = "change_devices"
- if (!audio.deviceModelsInitialized) {
- audio.refreshDevices();
- }
- }
-
- AppIcon {
- id: changeDevicesIcon
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- width: 20 * virtualstudio.uiScale
- height: 20 * virtualstudio.uiScale
- icon.source: "cog.svg"
- onClicked: {
- virtualstudio.windowState = "change_devices"
- if (!audio.deviceModelsInitialized) {
- audio.refreshDevices();
- }
- }
- }
- }
-
- Text {
- anchors.top: changeDevicesButton.bottom
- text: "Devices"
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale}
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
- }
- }
- }
-
- Rectangle {
- id: backgroundBorder
- width: parent.width
- height: 1
- anchors.top: layout.top
- color: strokeColor
- }
-
- Popup {
- id: feedbackDetectedModal
- padding: 1
- width: parent.width
- height: parent.height
- anchors.centerIn: parent
- dim: false
- modal: false
- focus: true
- closePolicy: Popup.NoAutoClose
-
- background: Rectangle {
- anchors.fill: parent
- color: "transparent"
- border.width: 1
- border.color: buttonStroke
- clip: true
- }
-
- contentItem: Rectangle {
- width: parent.width
- height: 232 * virtualstudio.uiScale
- color: backgroundColour
-
- Item {
- id: feedbackDetectedContent
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.leftMargin: 16 * virtualstudio.uiScale
- anchors.right: parent.right
-
- AppIcon {
- id: feedbackWarningIcon
- anchors.left: parent.left
- anchors.top: parent.top
- anchors.topMargin: 10 * virtualstudio.uiScale
- width: 32 * virtualstudio.uiScale
- height: 32 * virtualstudio.uiScale
- icon.source: "warning.svg"
- color: "#F21B1B"
- visible: showDeviceControls
- }
-
- AppIcon {
- id: feedbackWarningIconMinified
- anchors.left: parent.left
- anchors.verticalCenter: parent.verticalCenter
- height: 24 * virtualstudio.uiScale
- width: 24 * virtualstudio.uiScale
- icon.source: "warning.svg"
- color: "#F21B1B"
- visible: !showDeviceControls
- }
-
- Text {
- id: feedbackDetectedHeader
- anchors.top: parent.top
- anchors.topMargin: 10 * virtualstudio.uiScale
- anchors.left: feedbackWarningIcon.right
- anchors.leftMargin: 16 * virtualstudio.uiScale
- width: parent.width
- text: "Audio feedback detected!"
- font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- visible: showDeviceControls
- }
-
- Text {
- id: feedbackDetectedText
- anchors.top: feedbackDetectedHeader.bottom
- anchors.topMargin: 4 * virtualstudio.uiScale
- anchors.left: feedbackWarningIcon.right
- anchors.leftMargin: 16 * virtualstudio.uiScale
- width: parent.width
- text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
- font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- visible: showDeviceControls
- }
-
- Text {
- id: feedbackDetectedTextMinified
- anchors.verticalCenter: parent.verticalCenter
- anchors.left: feedbackWarningIcon.right
- anchors.leftMargin: 16 * virtualstudio.uiScale
- width: parent.width
- text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
- font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- visible: !showDeviceControls
- }
-
- Text {
- id: feedbackDetectedText2
- anchors.top: feedbackDetectedText.bottom
- anchors.topMargin: 2 * virtualstudio.uiScale
- anchors.left: feedbackWarningIcon.right
- anchors.leftMargin: 16 * virtualstudio.uiScale
- width: parent.width
- text: "You can disable this behavior under <b>Settings</b> > <b>Advanced</b>"
- textFormat: Text.RichText
- font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- visible: showDeviceControls
- }
-
- Button {
- id: closeFeedbackDetectedModalButton
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- width: 128 * virtualstudio.uiScale;
- height: 30 * virtualstudio.uiScale
- onClicked: feedbackDetectedModal.close()
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
- border.width: 1
- border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
- }
-
- Text {
- text: "Ok"
- font.family: "Poppins"
- font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- visible: showDeviceControls
- }
-
- Button {
- id: closeFeedbackDetectedModalButtonMinified
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- width: 80 * virtualstudio.uiScale
- height: 24 * virtualstudio.uiScale
- onClicked: feedbackDetectedModal.close()
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
- border.width: 1
- border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
- }
-
- Text {
- text: "Ok"
- font.family: "Poppins"
- font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- visible: !showDeviceControls
- }
- }
- }
- }
-
- Connections {
- target: virtualstudio
-
- function onFeedbackDetected() {
- if (virtualstudio.windowState === "connected") {
- feedbackDetectedModal.visible = true;
- }
- }
-
- function onCollapseDeviceControlsChanged(collapseDeviceControls) {
- showDeviceControls = getShowDeviceControls()
- }
-
- function onCurrentStudioChanged(currentStudio) {
- isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
- showDeviceControls = getShowDeviceControls()
- }
-
- function onConnectionStateChanged(connectionState) {
- isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
- showDeviceControls = getShowDeviceControls()
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Button {
- id: refreshButton
- text: "Refresh Devices"
-
- property int fontExtraSmall: 8
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property var onDeviceRefresh: function () { audio.refreshDevices(); };
-
- width: 144 * virtualstudio.uiScale;
- height: 30 * virtualstudio.uiScale
- palette.buttonText: textColour
- display: AbstractButton.TextBesideIcon
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- icon {
- source: "refresh.svg";
- color: textColour;
- }
- onClicked: { onDeviceRefresh(); }
- font {
- family: "Poppins"
- pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- height: 28 * virtualstudio.uiScale
- property string devicesWarningColour: "#F21B1B"
-
- AppIcon {
- id: devicesWarningIcon
- anchors.left: parent.left
- anchors.verticalCenter: parent.verticalCenter
- width: parent.height
- height: parent.height
- icon.source: "warning.svg"
- color: devicesWarningColour
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
-
- Text {
- id: warningOrErrorText
- text: Boolean(audio.devicesError) ? "Audio Configuration Error" : "Audio Configuration Warning"
- anchors.left: devicesWarningIcon.right
- anchors.leftMargin: 4 * virtualstudio.uiScale
- anchors.verticalCenter: devicesWarningIcon.verticalCenter
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale }
- color: devicesWarningColour
- }
-
- InfoTooltip {
- id: devicesWarningTooltip
- anchors.left: warningOrErrorText.right
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.top: devicesWarningIcon.top
- content: qsTr(audio.devicesError || audio.devicesWarning)
- iconColor: devicesWarningColour
- size: 16 * virtualstudio.uiScale
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
-
- MouseArea {
- id: devicesWarningToolTipArea
- anchors.top: devicesWarningIcon.top
- anchors.bottom: devicesWarningIcon.bottom
- anchors.left: devicesWarningIcon.left
- anchors.right: devicesWarningTooltip.right
- hoverEnabled: true
- onEntered: devicesWarningTooltip.showToolTip = true
- onExited: devicesWarningTooltip.showToolTip = false
- onClicked: {
- if (Boolean(audio.devicesError) && audio.devicesErrorHelpUrl !== "") {
- virtualstudio.openLink(audio.devicesErrorHelpUrl);
- } else if (Boolean(audio.devicesWarning) && audio.devicesWarningHelpUrl !== "") {
- virtualstudio.openLink(audio.devicesWarningHelpUrl);
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property int leftMargin: 16
- property int fontBig: 28
- property int fontMedium: 18
- property int fontSmall: 11
-
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
- property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
- property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
- property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7"
- property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
- property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
- property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
-
- AppIcon {
- id: ohnoImage
- y: 60
- anchors.horizontalCenter: parent.horizontalCenter
- width: 180
- height: 180
- icon.source: "sentiment_very_dissatisfied.svg"
- }
-
- Text {
- id: ohnoHeader
- text: "Oh no!"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ohnoImage.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- }
-
- Text {
- id: ohnoMessage
- text: virtualstudio.failedMessage || "Unable to process request - please try again later."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 400
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ohnoHeader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Button {
- id: backButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
- border.width: backButton.down ? 1 : 0
- border.color: buttonStroke
- layer.enabled: !backButton.down
- }
- onClicked: { virtualstudio.windowState = "browse" }
- width: 256 * virtualstudio.uiScale
- height: 42 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ohnoMessage.bottom
- anchors.topMargin: 60 * virtualstudio.uiScale
- Text {
- text: "Back"
- font.family: "Poppins"
- font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour)
- }
- visible: true
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- id: userFeedbackSurvey
-
- anchors.centerIn: parent
- width: 480 * virtualstudio.uiScale
- height: 232 * virtualstudio.uiScale
-
- property int leftHeaderMargin: 16
- property int fontBig: 28
- property int fontMedium: 12
- property int fontSmall: 10
- property int fontTiny: 8
- property int bodyMargin: 60
- property int rightMargin: 16
- property int bottomToolTipMargin: 8
- property int rightToolTipMargin: 4
-
- property string buttonColour: "#F2F3F3"
- property string buttonHoverColour: "#E7E8E8"
- property string buttonPressedColour: "#E7E8E8"
- property string buttonStroke: "#EAEBEB"
- property string buttonHoverStroke: "#B0B5B5"
- property string buttonPressedStroke: "#B0B5B5"
-
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
- property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-
- property string serverId: ""
-
- property int rating: 0
- property int hover: star1MouseArea.containsMouse ? 1 : star2MouseArea.containsMouse ? 2 : star3MouseArea.containsMouse ? 3 : star4MouseArea.containsMouse ? 4 : star5MouseArea.containsMouse ? 5 : 0
- property int currentView: (hover > 0 ? hover : rating)
- property bool submitted: false;
-
- property string message: ""
-
- Popup {
- id: userFeedbackModal
- padding: 1
- width: parent.width
- height: 300 * virtualstudio.uiScale
- anchors.centerIn: parent
- modal: true
- focus: true
- closePolicy: Popup.NoAutoClose
-
- background: Rectangle {
- anchors.fill: parent
- color: "transparent"
- radius: 6 * virtualstudio.uiScale
- border.width: 1
- border.color: buttonStroke
- clip: true
- }
-
- contentItem: Rectangle {
- width: parent.width
- height: parent.height
- color: backgroundColour
- radius: 6 * virtualstudio.uiScale
-
- Item {
- id: userFeedbackSurveyContent
- anchors.top: parent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.right: parent.right
- visible: !submitted
-
- Text {
- id: userFeedbackSurveyHeader
- anchors.top: parent.top
- anchors.topMargin: 16 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- text: "How did your session go?"
- font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- }
-
- Text {
- id: ratingItemInstructions
- anchors.top: userFeedbackSurveyHeader.bottom
- anchors.topMargin: 12 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- text: "Rate your session on a scale of 1 to 5"
- font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- }
-
- Item {
- id: ratingItem
- anchors.top: ratingItemInstructions.bottom
- anchors.topMargin: 4 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- height: 40 * virtualstudio.uiScale
- width: 200 * virtualstudio.uiScale
-
- Item {
- id: star1Container
- anchors.verticalCenter: parent.verticalCenter
- anchors.right: star2Container.left
- width: parent.width / 5
- height: parent.height
-
- AppIcon {
- id: star1Icon
- anchors.centerIn: parent
- width: currentView >= 1 ? parent.width : 20 * virtualstudio.uiScale
- height: currentView >= 1 ? parent.height : 20 * virtualstudio.uiScale
- icon.source: "star.svg"
- color: currentView >= 1 ? "#faaf00" : "#606060"
- }
-
- MouseArea {
- id: star1MouseArea
- anchors.fill: parent
- hoverEnabled: true
- onClicked: () => {
- if (rating === 1) {
- rating = 0;
- } else {
- rating = 1;
- }
- }
- }
- }
-
- Item {
- id: star2Container
- anchors.verticalCenter: parent.verticalCenter
- anchors.right: star3Container.left
- width: parent.width / 5
- height: parent.height
-
- AppIcon {
- id: star2Icon
- anchors.centerIn: parent
- width: currentView >= 2 ? parent.width : 20 * virtualstudio.uiScale
- height: currentView >= 2 ? parent.height : 20 * virtualstudio.uiScale
- icon.source: "star.svg"
- color: currentView >= 2 ? "#faaf00" : "#606060"
- }
-
- MouseArea {
- id: star2MouseArea
- anchors.fill: parent
- hoverEnabled: true
- onClicked: () => {
- if (rating === 2) {
- rating = 0;
- } else {
- rating = 2;
- }
- }
- }
- }
-
- Item {
- id: star3Container
- anchors.verticalCenter: parent.verticalCenter
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width / 5
- height: parent.height
-
- AppIcon {
- id: star3Icon
- anchors.centerIn: parent
- width: currentView >= 3 ? parent.width : 20 * virtualstudio.uiScale
- height: currentView >= 3 ? parent.height : 20 * virtualstudio.uiScale
- icon.source: "star.svg"
- color: currentView >= 3 ? "#faaf00" : "#606060"
- }
-
- MouseArea {
- id: star3MouseArea
- anchors.fill: parent
- hoverEnabled: true
- onClicked: () => {
- if (rating === 3) {
- rating = 0;
- } else {
- rating = 3;
- }
- }
- }
- }
-
- Item {
- id: star4Container
- anchors.verticalCenter: parent.verticalCenter
- anchors.left: star3Container.right
- width: parent.width / 5
- height: parent.height
-
- AppIcon {
- id: star4Icon
- anchors.centerIn: parent
- width: currentView >= 4 ? parent.width : 20 * virtualstudio.uiScale
- height: currentView >= 4 ? parent.height : 20 * virtualstudio.uiScale
- icon.source: "star.svg"
- color: currentView >= 4 ? "#faaf00" : "#606060"
- }
-
- MouseArea {
- id: star4MouseArea
- anchors.fill: parent
- hoverEnabled: true
- onClicked: () => {
- if (rating === 4) {
- rating = 0;
- } else {
- rating = 4;
- }
- }
- }
- }
-
- Item {
- id: star5Container
- anchors.verticalCenter: parent.verticalCenter
- anchors.left: star4Container.right
- width: parent.width / 5
- height: parent.height
-
- AppIcon {
- id: star5Icon
- anchors.centerIn: parent
- width: currentView >= 5 ? parent.width : 20 * virtualstudio.uiScale
- height: currentView >= 5 ? parent.height : 20 * virtualstudio.uiScale
- icon.source: "star.svg"
- color: currentView >= 5 ? "#faaf00" : "#606060"
- }
-
- MouseArea {
- id: star5MouseArea
- anchors.fill: parent
- hoverEnabled: true
- onClicked: () => {
- if (rating === 5) {
- rating = 0;
- } else {
- rating = 5;
- }
- }
- }
- }
- }
-
- ScrollView {
- id: messageBoxScrollArea
- anchors.left: parent.left
- anchors.leftMargin: 32 * virtualstudio.uiScale
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.top: ratingItem.bottom
- anchors.topMargin: 12 * virtualstudio.uiScale
- height: 64 * virtualstudio.uiScale
-
- TextArea {
- id: messageBox
- placeholderText: qsTr("(Optional) Let us know how we can improve your experience.")
- placeholderTextColor: textAreaTextColour
- color: textColour
- background: Rectangle {
- color: textAreaColour
- radius: 6 * virtualstudio.uiScale
- border.width: 1
- border.color: buttonStroke
- }
- }
- }
-
- Item {
- id: buttonsArea
- height: 32 * virtualstudio.uiScale
- width: 324 * virtualstudio.uiScale
- anchors.horizontalCenter: messageBoxScrollArea.horizontalCenter
- anchors.top: messageBoxScrollArea.bottom
- anchors.topMargin: 24 * virtualstudio.uiScale
-
- Button {
- id: userFeedbackButton
- anchors.right: buttonsArea.right
- anchors.horizontalCenter: buttonsArea.horizontalCenter
- anchors.verticalCenter: parent.buttonsArea
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- onClicked: () => {
- if (rating === 0 && messageBox.text === "") {
- userFeedbackModal.close();
- serverId = "";
- messageBox.clear();
- return;
- }
- virtualstudio.collectFeedbackSurvey(serverId, rating, messageBox.text);
- submitted = true;
- rating = 0;
- serverId = "";
- messageBox.clear();
- userFeedbackModal.height = 150 * virtualstudio.uiScale
- submittedFeedbackTimer.start();
- }
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke)
- }
-
- Text {
- text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit"
- font.family: "Poppins"
- font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: "#DB0A0A"
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- Timer {
- id: submittedFeedbackTimer
- interval: 5000; running: false; repeat: false
- onTriggered: () => {
- submitted = false;
- userFeedbackModal.height = 300 * virtualstudio.uiScale
- userFeedbackModal.close();
- }
- }
- }
- }
-
- Item {
- id: submittedFeedbackContent
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.right: parent.right
- visible: submitted
-
- Text {
- id: submittedFeedbackHeader
- anchors.top: submittedFeedbackContent.top
- anchors.topMargin: 24 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- text: "Thank you!"
- font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- }
-
- Text {
- id: submittedFeedbackText
- anchors.top: submittedFeedbackHeader.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- text: "Your feedback has been recorded."
- font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- }
-
- Button {
- id: closeButtonFeedback
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: submittedFeedbackText.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- onClicked: () => {
- submitted = false;
- userFeedbackModal.height = 300 * virtualstudio.uiScale
- userFeedbackModal.close();
- }
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke)
- }
-
- Text {
- text: "Close"
- font.family: "Poppins"
- font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
- }
- }
-
- Connections {
- target: virtualstudio
-
- function onOpenFeedbackSurveyModal(serverId) {
- userFeedbackSurvey.serverId = serverId;
- userFeedbackModal.open();
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
- property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
- property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
- property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
- property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
-
- Image {
- id: jtlogo
- source: "logo.svg"
- anchors.horizontalCenter: parent.horizontalCenter
- y: 35 * virtualstudio.uiScale
- width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale
- sourceSize: Qt.size(jtlogo.width,jtlogo.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
-
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- y: 168 * virtualstudio.uiScale
- text: "Sign in with a Virtual Studio account?"
- font.family: "Poppins"
- font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale
- color: textColour
- }
-
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- y: 219 * virtualstudio.uiScale
- text: "You'll be able to change your mind later"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- color: textColour
- }
-
- Button {
- id: vsButton
- background: Rectangle {
- radius: 10 * virtualstudio.uiScale
- color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke)
- layer.enabled: vsButton.hovered && !vsButton.down
- }
- onClicked: { virtualstudio.showFirstRun = false; virtualstudio.windowState = "login"; virtualstudio.toVirtualStudio(); }
- x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
- width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
- Text {
- text: "Yes"
- font.family: "Poppins"
- font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: "#DB0A0A"
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- Text {
- text: "• Seamless Audio & Video<br>• Recording & Livestreaming<br>• No Servers Required"
- textFormat: Text.StyledText
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- x: parent.width / 2 - (265 * virtualstudio.uiScale);
- y: 355 * virtualstudio.uiScale;
- width: 230 * virtualstudio.uiScale
- padding: 0
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- }
- Image {
- source: "JTVS.png"
- x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
- width: 234 * virtualstudio.uiScale; height: 195 * virtualstudio.uiScale;
- }
-
- Button {
- id: standardButton
- background: Rectangle {
- radius: 10 * virtualstudio.uiScale
- color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke)
- layer.enabled: standardButton.hovered && !standardButton.down
- }
- onClicked: { virtualstudio.toStandard(); }
- x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
- width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
- Text {
- text: "No"
- font.family: "Poppins"
- font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: "#DB0A0A"
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- Image {
- source: "JTOriginal.png"
- x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
- width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale;
- }
- Text {
- text: virtualstudio.psiBuild ? "• Connect via IP address<br>• Run a local hub server<br>• The Standard JackTrip experience" :
- "• Connect via IP address<br>• Run a local hub server<br>• The Classic JackTrip experience"
- textFormat: Text.StyledText
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- x: parent.width / 2 + (32 * virtualstudio.uiScale);
- y: 355 * virtualstudio.uiScale;
- width: 230 * virtualstudio.uiScale
- padding: 0
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
- color: textColour
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-Rectangle {
- id: footer
- width: parent.width
- height: 24
- anchors.bottom: parent.bottom
- color: backgroundColour
- clip: true
-
- property string statsOrange: "#b26a00"
- property string connectionStateColor: getConnectionStateColor()
- property variant networkStatsText: getNetworkStatsText()
-
- function getConnectionStateColor() {
- if (virtualstudio.connectionState == "Connected") {
- return meterGreen
- }
- if (virtualstudio.connectionState.includes("Disconnected") || virtualstudio.connectionState.includes("Error")) {
- return meterRed
- }
- if (studioStatus === "Starting" || virtualstudio.connectionState == "Connecting..." || virtualstudio.connectionState == "Reconnecting...") {
- return meterYellow
- }
- return "grey"
- }
-
- function getNetworkStatsText() {
- let minRtt = virtualstudio.networkStats.minRtt;
- let maxRtt = virtualstudio.networkStats.maxRtt;
- let avgRtt = virtualstudio.networkStats.avgRtt;
-
- let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed];
- if (virtualstudio.networkOutage) {
- return texts;
- }
-
- texts = ["Measuring...", "", "grey"];
- if (!minRtt || !maxRtt) {
- return texts;
- }
-
- texts[1] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms";
- let quality = "Poor";
- let color = meterRed;
- if (avgRtt < 10 && maxRtt < 15) {
- quality = "Excellent";
- color = meterGreen;
- } else if (avgRtt < 20 && maxRtt < 30) {
- quality = "Good";
- color = meterYellow;
- } else if (avgRtt < 30 && maxRtt < 40) {
- quality = "Fair";
- color = statsOrange;
- }
-
- texts[0] = quality
- texts[2] = color;
- return texts;
- }
-
- MouseArea {
- anchors.fill: parent
- propagateComposedEvents: false
- }
-
- RowLayout {
- id: layout
- anchors.fill: parent
- spacing: 4
-
- Rectangle {
- color: backgroundColour
- Layout.minimumWidth: 256
- Layout.preferredWidth: 512
- Layout.maximumWidth: 640
- Layout.fillHeight: true
- Layout.fillWidth: true
- visible: studioStatus === "Ready"
-
- AppIcon {
- id: connectionQualityIcon
- anchors.left: parent.left
- anchors.leftMargin: 8 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- width: 20 * virtualstudio.uiScale
- height: 20 * virtualstudio.uiScale
- icon.source: "speed.svg"
- }
-
- Text {
- id: connectionQualityText
- anchors.left: connectionQualityIcon.right
- anchors.leftMargin: 4 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- text: "Connection:"
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Text {
- id: connectionQualityName
- anchors.left: connectionQualityText.right
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- text: networkStatsText[0]
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: networkStatsText[2]
- }
-
- Text {
- id: connectionQualityTime
- anchors.left: connectionQualityName.right
- anchors.leftMargin: 8 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- text: networkStatsText[1]
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
- }
-
- Item {
- Layout.fillWidth: true
- Layout.fillHeight: true
- }
-
- Rectangle {
- color: backgroundColour
- Layout.minimumWidth: 96
- Layout.preferredWidth: 128
- Layout.maximumWidth: 160
- Layout.fillHeight: true
-
- Rectangle {
- id: connectionStatusDot
- anchors.right: connectionStatusText.left
- anchors.rightMargin: 4 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- width: 12
- height: connectionStatusDot.width
- radius: connectionStatusDot.height / 2
- color: connectionStateColor
- }
-
- Text {
- id: connectionStatusText
- anchors.right: parent.right
- anchors.rightMargin: 8 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- text: studioStatus === "Starting" ? "Starting..." : virtualstudio.connectionState
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
- }
- }
-
- Rectangle {
- id: backgroundBorder
- width: parent.width
- height: 1
- y: parent.height - footer.height
- color: buttonStroke
- }
-
- Connections {
- target: virtualstudio
-
- function onConnectionStateChanged() {
- connectionStatusDot.color = getConnectionStateColor()
- }
- function onNetworkStatsChanged() {
- networkStatsText = getNetworkStatsText();
- }
- function onUpdatedNetworkOutage() {
- networkStatsText = getNetworkStatsText();
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-import Qt5Compat.GraphicalEffects
-
-Item {
- required property string content
- required property int size
-
- width: size * virtualstudio.uiScale
- height: size * virtualstudio.uiScale
-
- property string iconSource: "help.svg"
- property string iconColor: ""
- property string backgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
- property bool showToolTip: false
-
- Item {
- anchors.fill: parent
-
- AppIcon {
- id: tooltipIcon
- anchors.centerIn: parent
- width: parent.width
- height: parent.height
- icon.source: iconSource
- color: iconColor
- }
-
- MouseArea {
- id: mouseArea
- anchors.fill: tooltipIcon
- hoverEnabled: true
- onEntered: showToolTip = true
- onExited: showToolTip = false
- }
-
- ToolTip {
- visible: showToolTip
- x: tooltipIcon.x + tooltipIcon.width
- y: tooltipIcon.y + tooltipIcon.height
-
- contentItem: Text {
- text: content
- font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- }
-
- background: Rectangle {
- color: backgroundColour
- radius: 4
- layer.enabled: true
- layer.effect: Glow {
- radius: 8
- color: "#66000000"
- transparentBorder: true
- }
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Button {
- property string url
- property string buttonText: "Learn more"
-
- width: 150 * virtualstudio.uiScale;
- height: 30 * virtualstudio.uiScale
-
- onClicked: {
- virtualstudio.openLink(url);
- }
-
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke)
- }
-
- Text {
- text: buttonText
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import VS 1.0
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- state: auth.authenticationStage
- states: [
- State {
- name: "unauthenticated"
- },
- State {
- name: "refreshing"
- },
- State {
- name: "polling"
- },
- State {
- name: "success"
- },
- State {
- name: "failed"
- }
- ]
-
- Rectangle {
- width: parent.width; height: parent.height
- color: backgroundColour
- }
-
- property bool codeCopied: false
- property int numFailures: 0;
-
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
- property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
- property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
- property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
- property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
- property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
- property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
- property string linkTextColour: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
- property string toolTipTextColour: codeCopied ? "#FAFBFB" : textColour
- property string toolTipBackgroundColour: codeCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3")
- property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string disabledButtonText: "#D3D4D4"
- property string errorTextColour: "#DB0A0A"
-
- property bool showCodeFlow: (loginScreen.state === "unauthenticated" && !auth.attemptingRefreshToken) || (loginScreen.state === "polling" || loginScreen.state === "failed" || (loginScreen.state === "success" && auth.authenticationMethod === "code flow"))
- property bool showLoading: (loginScreen.state === "unauthenticated" ** auth.attemptingRefreshToken) || loginScreen.state === "refreshing" || (loginScreen.state === "success" && auth.authenticationMethod === "refresh token")
-
- Clipboard {
- id: clipboard
- }
-
- Item {
- id: loginScreenHeader
- anchors.horizontalCenter: parent.horizontalCenter
- y: showCodeFlow ? 48 * virtualstudio.uiScale : 144 * virtualstudio.uiScale
-
- Image {
- id: loginLogo
- source: "logo.svg"
- x: parent.width / 2 - (150 * virtualstudio.uiScale);
- width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale
- sourceSize: Qt.size(loginLogo.width,loginLogo.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
-
- Image {
- source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
- anchors.bottom: loginLogo.bottom
- x: parent.width / 2 - (88 * virtualstudio.uiScale)
- width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale
- }
-
- Text {
- text: "Virtual Studio"
- font.family: "Poppins"
- font.pixelSize: 24 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- y: 80 * virtualstudio.uiScale
- color: textColour
- }
- }
-
- Item {
- id: codeFlow
- anchors.horizontalCenter: parent.horizontalCenter
- y: 68 * virtualstudio.uiScale
- height: parent.height - codeFlow.y
- visible: showCodeFlow
- width: parent.width
-
- Text {
- id: deviceVerificationExplanation
- text: `To get started, please sign in and confirm the following code using your web browser. Return here when you are done.`
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- y: 128 * virtualstudio.uiScale
- width: 500 * virtualstudio.uiScale;
- visible: true
- color: textColour
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
- textFormat: Text.RichText
- onLinkActivated: link => {
- if (!Boolean(auth.verificationCode)) {
- return;
- }
- virtualstudio.openLink(link)
- }
- }
-
- AppIcon {
- id: successIcon
- y: 224 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- width: 96 * virtualstudio.uiScale
- height: 96 * virtualstudio.uiScale
- icon.source: "check.svg"
- color: "green"
- visible: loginScreen.state === "success"
- }
-
- Text {
- id: deviceVerificationCode
- text: auth.verificationCode || ((numFailures >= 5) ? "Error" : "Loading...");
- font.family: "Poppins"
- font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
- font.letterSpacing: Boolean(auth.verificationCode) ? 8 : 1
- anchors.horizontalCenter: parent.horizontalCenter
- y: 196 * virtualstudio.uiScale
- width: 360 * virtualstudio.uiScale;
- visible: !auth.isAuthenticated
- color: Boolean(auth.verificationCode) ? textColour : disabledButtonText
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
-
- Timer {
- id: copiedResetTimer
- interval: 2000; running: false; repeat: false
- onTriggered: codeCopied = false;
- }
-
- MouseArea {
- id: deviceVerificationCodeMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- enabled: Boolean(auth.verificationCode)
- hoverEnabled: true
- onClicked: () => {
- codeCopied = true;
- clipboard.setText(auth.verificationCode);
- copiedResetTimer.restart()
- }
- }
-
- ToolTip {
- parent: deviceVerificationCode
- visible: loginScreen.state === "polling" && deviceVerificationCodeMouseArea.containsMouse
- delay: 100
- contentItem: Rectangle {
- color: toolTipBackgroundColour
- radius: 3
- anchors.fill: parent
- layer.enabled: true
- border.width: 1
- border.color: tooltipStroke
-
- Text {
- anchors.centerIn: parent
- font { family: "Poppins"; pixelSize: 8 * virtualstudio.fontScale * virtualstudio.uiScale}
- text: codeCopied ? qsTr("📋 Copied code to clipboard") : qsTr("📋 Copy code to Clipboard")
- color: toolTipTextColour
- }
- }
- background: Rectangle {
- color: "transparent"
- }
- }
- }
-
- Button {
- id: loginButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : buttonStroke)
- layer.enabled: !loginButton.down
- }
- onClicked: {
- if (auth.verificationCode && auth.verificationUrl) {
- virtualstudio.openLink(auth.verificationUrl);
- }
- }
- anchors.horizontalCenter: parent.horizontalCenter
- y: 260 * virtualstudio.uiScale
- width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
- Text {
- text: "Sign In"
- font.family: "Poppins"
- font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour)
- }
- visible: !auth.isAuthenticated
- }
-
- Text {
- id: authFailedText
- text: "There was an error trying to sign in. Please try again."
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.bottom: loginScreenFooter.top
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- visible: (loginScreen.state === "failed" || numFailures > 0) && loginScreen.state !== "success"
- color: errorTextColour
- }
-
- Item {
- id: loginScreenFooter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.bottom: parent.bottom
- anchors.bottomMargin: 24 * virtualstudio.uiScale
- width: parent.width
- height: 48 * virtualstudio.uiScale
-
- property bool showBackButton: !virtualstudio.vsFtux
-
- Item {
- id: backButton
- visible: parent.showBackButton
- anchors.verticalCenter: parent.verticalCenter
- x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale
- width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
- Text {
- text: "Back"
- font.family: "Poppins"
- font.underline: true
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- color: textColour
- }
- MouseArea {
- anchors.fill: parent
- onClicked: () => { if (!auth.isAuthenticated) { virtualstudio.windowState = "start"; } }
- cursorShape: Qt.PointingHandCursor
- }
- }
-
- Item {
- id: resetCodeButton
- visible: true
- x: parent.showBackButton ? (parent.x + parent.width / 2) + 8 * virtualstudio.uiScale : (parent.x + parent.width / 2) - resetCodeButton.width / 2
- anchors.verticalCenter: parent.verticalCenter
- width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
- Text {
- text: "Reset Code"
- font.family: "Poppins"
- font.underline: true
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- color: textColour
- }
- MouseArea {
- anchors.fill: parent
- onClicked: () => {
- if (auth.verificationCode && auth.verificationUrl) {
- auth.resetCode();
- }
- }
- cursorShape: Qt.PointingHandCursor
- }
- }
- }
- }
-
- Item {
- id: refreshToken
- anchors.horizontalCenter: parent.horizontalCenter
- y: 108 * virtualstudio.uiScale
- visible: showLoading
-
- Text {
- id: loadingViaRefreshToken
- text: "Logging In...";
- font.family: "Poppins"
- font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- y: 208 * virtualstudio.uiScale
- width: 360 * virtualstudio.uiScale;
- color: textColour
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
- }
- }
-
- Connections {
- target: auth
- function onUpdatedAuthenticationStage (stage) {
- loginScreen.state = stage;
- if (stage === "failed") {
- numFailures = numFailures + 1;
- if (numFailures < 5 && !virtualstudio.hasRefreshToken) {
- virtualstudio.login();
- }
- }
- if (stage === "success") {
- numFailures = 0;
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- required property var model
- property int bins: Math.max(15, width/20)
- property int innerMargin: 2 * virtualstudio.uiScale
- property int boxRadius: 3 * virtualstudio.uiScale
- property int boxThickness: 12
- required property bool clipped
- property bool enabled: true
- property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
- property string meterRed: "#F21B1B"
-
- Item {
- id: meters
- width: parent.width - boxThickness - innerMargin
- height: parent.height
-
- MeterBars {
- id: leftchannel
- x: 0
- y: 0
- width: parent.width
- height: boxThickness
- level: parent.parent.model[0]
- enabled: parent.parent.enabled
- }
-
- MeterBars {
- id: rightchannel
- x: 0;
- anchors.top: leftchannel.bottom
- anchors.topMargin: innerMargin
- width: parent.width
- height: boxThickness
- level: parent.parent.model[1]
- enabled: parent.parent.enabled
- }
- }
-
- Rectangle {
- id: clipIndicator
- y: 0
- anchors.left: meters.right
- anchors.leftMargin: innerMargin
-
- width: boxThickness
- height: leftchannel.height + rightchannel.height + innerMargin
- radius: boxRadius
- color: clipped ? meterRed : meterColor
- }
-}
\ No newline at end of file
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-Item {
- required property var level
- required property var enabled
- property int boxHeight: height
- property int boxWidth: (width / bins) - innerMargin
- property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
- property string meterGreen: "#61C554"
- property string meterYellow: "#F5BF4F"
- property string meterRed: "#F21B1B"
-
- function getBoxColor (idx) {
- // Case where the meter should not be filled
- if (!enabled || level <= (idx / bins)) {
- return meterColor;
- }
- // Case where the meter should be filled
- let fillColor = meterGreen;
- if (idx > 0.5*bins && idx <= 0.8*bins) {
- fillColor = meterYellow;
- } else if (idx > 0.8*bins) {
- fillColor = meterRed;
- }
- return fillColor;
- }
-
- RowLayout {
- anchors.fill: parent
- spacing: innerMargin
-
- Repeater {
- model: bins
- Rectangle {
- Layout.fillHeight: true
- Layout.fillWidth: true
- x: (boxWidth) * index + innerMargin * index;
- y: 0
- z: 1
- width: boxWidth
- height: boxHeight
- color: getBoxColor(index)
- radius: boxRadius
- }
- }
- }
-}
+++ /dev/null
-//*****************************************************************
-/*
- QJackTrip: Bringing a graphical user interface to JackTrip, a
- system for high quality audio network performance over the
- internet.
-
- Copyright (c) 2020 Aaron Wyatt.
-
- This file is part of QJackTrip.
-
- QJackTrip is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- QJackTrip 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 General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with QJackTrip. If not, see <https://www.gnu.org/licenses/>.
-*/
-//*****************************************************************
-
-#ifndef __NONAP_H__
-#define __NONAP_H__
-
-#include <objc/objc.h>
-
-class NoNap
-{
- public:
- NoNap();
- ~NoNap();
-
- void disableNap();
- void enableNap();
-
- private:
- id m_activity;
- bool m_preventNap;
-};
-
-#endif // __NONAP_H__
+++ /dev/null
-//*****************************************************************
-/*
- QJackTrip: Bringing a graphical user interface to JackTrip, a
- system for high quality audio network performance over the
- internet.
-
- Copyright (c) 2020 Aaron Wyatt.
-
- This file is part of QJackTrip.
-
- QJackTrip is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- QJackTrip 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 General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with QJackTrip. If not, see <https://www.gnu.org/licenses/>.
-*/
-//*****************************************************************
-
-#include "NoNap.h"
-#include <Foundation/Foundation.h>
-
-NoNap::NoNap() :
- m_preventNap(false)
-{}
-
-void NoNap::disableNap()
-{
- if (m_preventNap) {
- return;
- }
- m_preventNap = true;
- m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical | NSActivityUserInitiated reason:@"Disable App Nap"];
- [m_activity retain];
-}
-
-void NoNap::enableNap()
-{
- if (!m_preventNap) {
- return;
- }
- m_preventNap = false;
- [[NSProcessInfo processInfo] endActivity:m_activity];
- [m_activity release];
-}
-
-NoNap::~NoNap()
-{
- if (m_preventNap) {
- enableNap();
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property int fontBig: 20
- property int fontMedium: 13
- property int fontSmall: 11
- property int fontExtraSmall: 8
-
- property string saveButtonBackgroundColour: "#F2F3F3"
- property string saveButtonStroke: "#EAEBEB"
- property string saveButtonText: "#DB0A0A"
-
- Item {
- id: requestMicPermissionsItem
- width: parent.width; height: parent.height
- visible: permissions.micPermission == "unknown"
-
- AppIcon {
- id: microphonePrompt
- y: 60
- anchors.horizontalCenter: parent.horizontalCenter
- width: 260
- height: 250
- icon.source: "Prompt.svg"
- }
-
- Image {
- id: micLogo
- source: "logo.svg"
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: microphonePrompt.top
- anchors.topMargin: 18 * virtualstudio.uiScale
- width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
- sourceSize: Qt.size(micLogo.width,micLogo.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
-
- Button {
- id: showPromptButton
- width: 112 * virtualstudio.uiScale
- height: 30 * virtualstudio.uiScale
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 2
- border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: showPromptButton.hovered && !showPromptButton.down
- }
- onClicked: {
- permissions.getMicPermission();
- }
- anchors.right: microphonePrompt.right
- anchors.rightMargin: 13.5 * virtualstudio.uiScale
- anchors.bottomMargin: 17 * virtualstudio.uiScale
- anchors.bottom: microphonePrompt.bottom
- Text {
- text: "OK"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- Text {
- id: micPermissionsHeader
- text: "JackTrip needs your sounds!"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: microphonePrompt.bottom
- anchors.topMargin: 48 * virtualstudio.uiScale
- }
-
- Text {
- id: micPermissionsSubheader1
- text: "JackTrip requires permission to use your microphone."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 400
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: micPermissionsHeader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: micPermissionsSubheader2
- text: "Click ‘OK’ to give JackTrip access to your microphone, instrument, or other audio device."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 400
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: micPermissionsSubheader1.bottom
- anchors.topMargin: 24 * virtualstudio.uiScale
- }
- }
-
- Item {
- id: noMicItem
- width: parent.width; height: parent.height
- visible: permissions.micPermission == "denied"
-
- AppIcon {
- id: noMic
- y: 60
- anchors.horizontalCenter: parent.horizontalCenter
- width: 109.27
- height: 170
- icon.source: "micoff.svg"
- }
-
- Button {
- id: openSettingsButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: openSettingsButton.hovered && !openSettingsButton.down
- }
- onClicked: {
- permissions.openSystemPrivacy();
- }
- anchors.right: parent.right
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Open Privacy Settings"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- Text {
- id: noMicHeader
- text: "JackTrip can't hear you!"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: noMic.bottom
- anchors.topMargin: 48 * virtualstudio.uiScale
- }
-
- Text {
- id: noMicSubheader1
- text: "JackTrip requires permission to use your microphone."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 400
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: noMicHeader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: noMicSubheader2
- text: "Click 'Open Privacy Settings' to give JackTrip permission to access your microphone, instrument, or other audio device."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 400
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: noMicSubheader1.bottom
- anchors.topMargin: 24 * virtualstudio.uiScale
- }
- }
-
- Connections {
- target: permissions
-
- function onMicPermissionUpdated() {
- if (permissions.micPermission === "granted") {
- if (virtualstudio.studioToJoin === "") {
- virtualstudio.windowState = "browse";
- } else {
- virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
- virtualstudio.joinStudio();
- }
- }
- }
- }
-
-}
+++ /dev/null
-<svg width="260" height="250" viewBox="0 0 260 250" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="1" y="1" width="258" height="248" rx="9" stroke="#0F0D0D" stroke-width="2"/>
-<rect x="18" y="205" width="108" height="26" rx="5" stroke="#0F0D0D" stroke-width="2"/>
-<path d="M42.1189 222H44.9021C47.4861 222 48.9158 220.4 48.9158 217.775V217.764C48.9158 215.145 47.4802 213.545 44.9021 213.545H42.1189V222ZM42.9216 221.273V214.271H44.8552C46.9177 214.271 48.0955 215.59 48.0955 217.77V217.781C48.0955 219.961 46.9236 221.273 44.8552 221.273H42.9216ZM53.151 222.105C54.9147 222.105 56.0221 220.857 56.0221 218.848V218.836C56.0221 216.826 54.9147 215.584 53.151 215.584C51.3873 215.584 50.2857 216.826 50.2857 218.836V218.848C50.2857 220.857 51.3873 222.105 53.151 222.105ZM53.151 221.414C51.8502 221.414 51.0768 220.441 51.0768 218.848V218.836C51.0768 217.242 51.8502 216.275 53.151 216.275C54.4518 216.275 55.2252 217.242 55.2252 218.836V218.848C55.2252 220.441 54.4518 221.414 53.151 221.414ZM57.6323 222H58.4057V218.25C58.4057 217.061 59.1616 216.275 60.2749 216.275C61.3706 216.275 61.9155 216.891 61.9155 218.092V222H62.6889V217.898C62.6889 216.428 61.8862 215.584 60.4799 215.584C59.4956 215.584 58.8159 216.035 58.4819 216.797H58.4057V215.689H57.6323V222ZM64.598 216.662H65.1898L66.0335 213.545H65.1312L64.598 216.662ZM70.0871 222.041C70.2746 222.041 70.4503 222.023 70.6261 221.994V221.326C70.4679 221.344 70.3507 221.35 70.175 221.35C69.4601 221.35 69.1847 221.027 69.1847 220.254V216.34H70.6261V215.689H69.1847V214.049H68.382V215.689H67.3683V216.34H68.382V220.43C68.382 221.578 68.88 222.041 70.0871 222.041ZM74.8251 222H75.6688L76.6063 219.41H80.1629L81.1004 222H81.9442L78.7801 213.545H77.9833L74.8251 222ZM78.3465 214.594H78.4168L79.911 218.719H76.8524L78.3465 214.594ZM83.3845 222H84.1579V213.176H83.3845V222ZM86.2076 222H86.981V213.176H86.2076V222ZM91.5209 222.105C93.2846 222.105 94.392 220.857 94.392 218.848V218.836C94.392 216.826 93.2846 215.584 91.5209 215.584C89.7572 215.584 88.6557 216.826 88.6557 218.836V218.848C88.6557 220.857 89.7572 222.105 91.5209 222.105ZM91.5209 221.414C90.2201 221.414 89.4467 220.441 89.4467 218.848V218.836C89.4467 217.242 90.2201 216.275 91.5209 216.275C92.8217 216.275 93.5951 217.242 93.5951 218.836V218.848C93.5951 220.441 92.8217 221.414 91.5209 221.414ZM96.969 222H97.7776L99.3303 216.762H99.4065L100.959 222H101.774L103.573 215.689H102.799L101.387 221.045H101.317L99.758 215.689H98.9905L97.4319 221.045H97.3616L95.9612 215.689H95.1819L96.969 222Z" fill="#272525"/>
-<path d="M141.5 205H239.5C242.261 205 244.5 207.239 244.5 210V226C244.5 228.761 242.261 231 239.5 231H141.5C138.739 231 136.5 228.761 136.5 226V210C136.5 207.239 138.739 205 141.5 205Z" fill="white" stroke="#0F0D0D" stroke-width="2"/>
-<path d="M185.77 213.328C183.286 213.328 181.698 215.027 181.698 217.77C181.698 220.512 183.262 222.217 185.77 222.217C188.272 222.217 189.831 220.512 189.831 217.77C189.831 215.033 188.266 213.328 185.77 213.328ZM185.77 214.881C187.147 214.881 188.026 216 188.026 217.77C188.026 219.533 187.147 220.664 185.77 220.664C184.381 220.664 183.508 219.533 183.508 217.77C183.508 216 184.399 214.881 185.77 214.881ZM193.228 222V219.451L194.066 218.461L196.521 222H198.642L195.343 217.254L198.425 213.545H196.456L193.333 217.312H193.228V213.545H191.458V222H193.228Z" fill="#272525"/>
-<path d="M20.1581 104.84L18.9647 108.814H20.7801L21.5164 104.84H20.1581ZM23.0716 104.84L21.9735 108.814H23.7889L24.43 104.84H23.0716ZM24.6637 111.309C24.6637 113.105 25.8951 114.235 27.774 114.235C29.7164 114.235 30.8844 113.13 30.8844 111.156V104.84H28.9674V111.144C28.9674 112.045 28.5357 112.521 27.7486 112.521C27.0123 112.521 26.5299 112.045 26.5172 111.309H24.6637ZM35.3011 112.718C34.66 112.718 34.2093 112.4 34.2093 111.88C34.2093 111.378 34.5965 111.093 35.39 111.036L36.8055 110.947V111.461C36.8055 112.172 36.1581 112.718 35.3011 112.718ZM34.6917 114.108C35.5995 114.108 36.3612 113.727 36.723 113.086H36.8373V114H38.6273V109.22C38.6273 107.722 37.5799 106.846 35.7264 106.846C33.9681 106.846 32.7811 107.659 32.6605 108.954H34.3617C34.514 108.535 34.9583 108.306 35.6249 108.306C36.3866 108.306 36.8055 108.636 36.8055 109.22V109.792L35.1107 109.893C33.3714 109.995 32.4002 110.731 32.4002 112C32.4002 113.283 33.346 114.108 34.6917 114.108ZM46.586 109.468C46.4337 107.875 45.3229 106.846 43.4693 106.846C41.2794 106.846 40.0479 108.16 40.0479 110.483C40.0479 112.832 41.2857 114.152 43.4693 114.152C45.2911 114.152 46.4337 113.143 46.586 111.575H44.8595C44.7198 112.267 44.2247 112.635 43.4693 112.635C42.4791 112.635 41.9142 111.88 41.9142 110.483C41.9142 109.106 42.4728 108.363 43.4693 108.363C44.2564 108.363 44.7325 108.801 44.8595 109.468H46.586ZM50.0062 109.734H49.8919V104.326H48.0448V114H49.8919V111.677L50.3998 111.156L52.4437 114H54.6336L51.7835 110.001L54.4496 106.999H52.3485L50.0062 109.734ZM59.7169 114V106.478H62.4654V104.84H55.0514V106.478H57.7999V114H59.7169ZM63.1878 114H65.035V110.122C65.035 109.138 65.7269 108.541 66.7362 108.541C67.0345 108.541 67.4662 108.598 67.6122 108.655V106.973C67.4535 106.916 67.1424 106.884 66.8885 106.884C65.9999 106.884 65.2762 107.417 65.0921 108.116H64.9779V106.999H63.1878V114ZM68.8107 114H70.6578V106.999H68.8107V114ZM69.7374 106.021C70.3976 106.021 70.8292 105.634 70.8292 105.088C70.8292 104.536 70.3976 104.155 69.7374 104.155C69.0709 104.155 68.6456 104.536 68.6456 105.088C68.6456 105.634 69.0709 106.021 69.7374 106.021ZM76.5218 106.884C75.557 106.884 74.7635 107.36 74.3827 108.147H74.2684V106.999H72.4784V116.323H74.3255V112.927H74.4398C74.7826 113.67 75.5506 114.108 76.5536 114.108C78.3055 114.108 79.3783 112.756 79.3783 110.496C79.3783 108.23 78.2928 106.884 76.5218 106.884ZM75.8934 112.565C74.9159 112.565 74.3065 111.785 74.3065 110.502C74.3065 109.22 74.9159 108.433 75.8998 108.433C76.8837 108.433 77.4803 109.214 77.4803 110.496C77.4803 111.791 76.89 112.565 75.8934 112.565ZM82.2145 108.814L83.3063 104.84H81.4972L80.8497 108.814H82.2145ZM85.128 108.814L86.3151 104.84H84.506L83.7633 108.814H85.128ZM100.95 106.999H99.1158L98.0748 112.02H97.9606L96.7291 106.999H94.9645L93.7394 112.02H93.6251L92.5841 106.999H90.7115L92.5778 114H94.5201L95.7579 109.169H95.8722L97.1227 114H99.0904L100.95 106.999ZM105.031 114.152C107.182 114.152 108.477 112.788 108.477 110.496C108.477 108.224 107.163 106.846 105.031 106.846C102.898 106.846 101.584 108.23 101.584 110.496C101.584 112.788 102.879 114.152 105.031 114.152ZM105.031 112.642C104.04 112.642 103.482 111.861 103.482 110.496C103.482 109.15 104.047 108.357 105.031 108.357C106.008 108.357 106.579 109.15 106.579 110.496C106.579 111.854 106.015 112.642 105.031 112.642ZM116.271 106.999H114.424V111.036C114.424 111.969 113.929 112.546 113.008 112.546C112.158 112.546 111.72 112.051 111.72 111.086V106.999H109.873V111.562C109.873 113.188 110.812 114.152 112.323 114.152C113.383 114.152 114.037 113.689 114.367 112.876H114.481V114H116.271V106.999ZM118.136 114H119.983V104.326H118.136V114ZM124.362 114.108C125.333 114.108 126.127 113.657 126.501 112.902H126.615V114H128.405V104.326H126.558V108.116H126.45C126.089 107.341 125.308 106.884 124.362 106.884C122.604 106.884 121.512 108.262 121.512 110.49C121.512 112.724 122.597 114.108 124.362 114.108ZM124.99 108.433C125.974 108.433 126.577 109.227 126.577 110.502C126.577 111.785 125.981 112.565 124.99 112.565C124 112.565 123.41 111.791 123.41 110.496C123.41 109.214 124.006 108.433 124.99 108.433ZM133.633 114H135.481V104.326H133.633V114ZM137.365 114H139.212V106.999H137.365V114ZM138.291 106.021C138.952 106.021 139.383 105.634 139.383 105.088C139.383 104.536 138.952 104.155 138.291 104.155C137.625 104.155 137.2 104.536 137.2 105.088C137.2 105.634 137.625 106.021 138.291 106.021ZM143.057 109.734H142.943V104.326H141.096V114H142.943V111.677L143.451 111.156L145.495 114H147.685L144.835 110.001L147.501 106.999H145.4L143.057 109.734ZM151.41 108.262C152.273 108.262 152.831 108.839 152.87 109.766H149.886C149.95 108.858 150.553 108.262 151.41 108.262ZM152.908 112.007C152.711 112.47 152.209 112.73 151.479 112.73C150.515 112.73 149.905 112.083 149.88 111.042V110.947H154.685V110.382C154.685 108.16 153.466 106.846 151.403 106.846C149.321 106.846 148.039 108.255 148.039 110.534C148.039 112.807 149.296 114.152 151.429 114.152C153.142 114.152 154.349 113.327 154.628 112.007H152.908ZM159.976 105.335V107.068H158.885V108.522H159.976V112.108C159.976 113.473 160.649 114.025 162.35 114.025C162.706 114.025 163.049 113.987 163.277 113.943V112.534C163.1 112.553 162.973 112.559 162.731 112.559C162.103 112.559 161.824 112.28 161.824 111.67V108.522H163.277V107.068H161.824V105.335H159.976ZM167.821 114.152C169.973 114.152 171.268 112.788 171.268 110.496C171.268 108.224 169.954 106.846 167.821 106.846C165.688 106.846 164.374 108.23 164.374 110.496C164.374 112.788 165.669 114.152 167.821 114.152ZM167.821 112.642C166.831 112.642 166.272 111.861 166.272 110.496C166.272 109.15 166.837 108.357 167.821 108.357C168.798 108.357 169.37 109.15 169.37 110.496C169.37 111.854 168.805 112.642 167.821 112.642ZM178.552 112.718C177.911 112.718 177.461 112.4 177.461 111.88C177.461 111.378 177.848 111.093 178.641 111.036L180.057 110.947V111.461C180.057 112.172 179.409 112.718 178.552 112.718ZM177.943 114.108C178.851 114.108 179.612 113.727 179.974 113.086H180.089V114H181.879V109.22C181.879 107.722 180.831 106.846 178.978 106.846C177.219 106.846 176.032 107.659 175.912 108.954H177.613C177.765 108.535 178.21 108.306 178.876 108.306C179.638 108.306 180.057 108.636 180.057 109.22V109.792L178.362 109.893C176.623 109.995 175.652 110.731 175.652 112C175.652 113.283 176.597 114.108 177.943 114.108ZM189.837 109.468C189.685 107.875 188.574 106.846 186.721 106.846C184.531 106.846 183.299 108.16 183.299 110.483C183.299 112.832 184.537 114.152 186.721 114.152C188.542 114.152 189.685 113.143 189.837 111.575H188.111C187.971 112.267 187.476 112.635 186.721 112.635C185.73 112.635 185.165 111.88 185.165 110.483C185.165 109.106 185.724 108.363 186.721 108.363C187.508 108.363 187.984 108.801 188.111 109.468H189.837ZM197.428 109.468C197.276 107.875 196.165 106.846 194.311 106.846C192.121 106.846 190.89 108.16 190.89 110.483C190.89 112.832 192.128 114.152 194.311 114.152C196.133 114.152 197.276 113.143 197.428 111.575H195.701C195.562 112.267 195.067 112.635 194.311 112.635C193.321 112.635 192.756 111.88 192.756 110.483C192.756 109.106 193.315 108.363 194.311 108.363C195.098 108.363 195.574 108.801 195.701 109.468H197.428ZM201.851 108.262C202.714 108.262 203.273 108.839 203.311 109.766H200.328C200.391 108.858 200.994 108.262 201.851 108.262ZM203.349 112.007C203.152 112.47 202.651 112.73 201.921 112.73C200.956 112.73 200.347 112.083 200.321 111.042V110.947H205.126V110.382C205.126 108.16 203.908 106.846 201.845 106.846C199.763 106.846 198.48 108.255 198.48 110.534C198.48 112.807 199.737 114.152 201.87 114.152C203.584 114.152 204.79 113.327 205.069 112.007H203.349ZM206.433 109.055C206.433 110.134 207.093 110.782 208.451 111.08L209.721 111.366C210.337 111.499 210.603 111.708 210.603 112.051C210.603 112.502 210.108 112.8 209.391 112.8C208.654 112.8 208.204 112.527 208.064 112.051H206.261C206.388 113.397 207.506 114.152 209.353 114.152C211.187 114.152 212.438 113.238 212.438 111.842C212.438 110.794 211.828 110.223 210.47 109.925L209.156 109.639C208.508 109.493 208.21 109.284 208.21 108.941C208.21 108.497 208.699 108.198 209.359 108.198C210.045 108.198 210.47 108.478 210.565 108.928H212.273C212.171 107.589 211.124 106.846 209.346 106.846C207.601 106.846 206.433 107.729 206.433 109.055ZM213.617 109.055C213.617 110.134 214.277 110.782 215.636 111.08L216.905 111.366C217.521 111.499 217.788 111.708 217.788 112.051C217.788 112.502 217.292 112.8 216.575 112.8C215.839 112.8 215.388 112.527 215.248 112.051H213.446C213.573 113.397 214.69 114.152 216.537 114.152C218.371 114.152 219.622 113.238 219.622 111.842C219.622 110.794 219.013 110.223 217.654 109.925L216.34 109.639C215.693 109.493 215.394 109.284 215.394 108.941C215.394 108.497 215.883 108.198 216.543 108.198C217.229 108.198 217.654 108.478 217.749 108.928H219.457C219.355 107.589 218.308 106.846 216.531 106.846C214.785 106.846 213.617 107.729 213.617 109.055ZM224.913 105.335V107.068H223.822V108.522H224.913V112.108C224.913 113.473 225.586 114.025 227.288 114.025C227.643 114.025 227.986 113.987 228.214 113.943V112.534C228.037 112.553 227.91 112.559 227.668 112.559C227.04 112.559 226.761 112.28 226.761 111.67V108.522H228.214V107.068H226.761V105.335H224.913ZM229.768 114H231.615V109.982C231.615 109.074 232.149 108.458 233.075 108.458C233.913 108.458 234.364 108.96 234.364 109.931V114H236.211V109.493C236.211 107.811 235.316 106.865 233.812 106.865C232.783 106.865 232.022 107.348 231.698 108.147H231.584V104.326H229.768V114ZM240.983 108.262C241.847 108.262 242.405 108.839 242.443 109.766H239.46C239.523 108.858 240.126 108.262 240.983 108.262ZM242.481 112.007C242.285 112.47 241.783 112.73 241.053 112.73C240.088 112.73 239.479 112.083 239.454 111.042V110.947H244.259V110.382C244.259 108.16 243.04 106.846 240.977 106.846C238.895 106.846 237.613 108.255 237.613 110.534C237.613 112.807 238.87 114.152 241.002 114.152C242.716 114.152 243.922 113.327 244.202 112.007H242.481ZM91.5539 130H93.4011V125.785C93.4011 125.004 93.9153 124.439 94.6389 124.439C95.3625 124.439 95.7942 124.865 95.7942 125.607V130H97.5715V125.715C97.5715 124.973 98.0539 124.439 98.803 124.439C99.5837 124.439 99.9709 124.852 99.9709 125.684V130H101.818V125.195C101.818 123.754 100.948 122.846 99.552 122.846C98.5744 122.846 97.7683 123.36 97.4446 124.141H97.3303C97.051 123.329 96.3782 122.846 95.3943 122.846C94.4739 122.846 93.7439 123.341 93.4582 124.141H93.344V122.999H91.5539V130ZM103.575 130H105.422V122.999H103.575V130ZM104.502 122.021C105.162 122.021 105.594 121.634 105.594 121.088C105.594 120.536 105.162 120.155 104.502 120.155C103.835 120.155 103.41 120.536 103.41 121.088C103.41 121.634 103.835 122.021 104.502 122.021ZM113.438 125.468C113.286 123.875 112.175 122.846 110.322 122.846C108.132 122.846 106.9 124.16 106.9 126.483C106.9 128.832 108.138 130.152 110.322 130.152C112.143 130.152 113.286 129.143 113.438 127.575H111.712C111.572 128.267 111.077 128.635 110.322 128.635C109.331 128.635 108.766 127.88 108.766 126.483C108.766 125.106 109.325 124.363 110.322 124.363C111.109 124.363 111.585 124.801 111.712 125.468H113.438ZM114.833 130H116.681V126.122C116.681 125.138 117.373 124.541 118.382 124.541C118.68 124.541 119.112 124.598 119.258 124.655V122.973C119.099 122.916 118.788 122.884 118.534 122.884C117.646 122.884 116.922 123.417 116.738 124.116H116.624V122.999H114.833V130ZM123.383 130.152C125.534 130.152 126.829 128.788 126.829 126.496C126.829 124.224 125.515 122.846 123.383 122.846C121.25 122.846 119.936 124.23 119.936 126.496C119.936 128.788 121.231 130.152 123.383 130.152ZM123.383 128.642C122.392 128.642 121.834 127.861 121.834 126.496C121.834 125.15 122.399 124.357 123.383 124.357C124.36 124.357 124.931 125.15 124.931 126.496C124.931 127.854 124.366 128.642 123.383 128.642ZM132.332 122.884C131.367 122.884 130.573 123.36 130.192 124.147H130.078V122.999H128.288V132.323H130.135V128.927H130.25C130.592 129.67 131.36 130.108 132.363 130.108C134.115 130.108 135.188 128.756 135.188 126.496C135.188 124.23 134.103 122.884 132.332 122.884ZM131.703 128.565C130.726 128.565 130.116 127.785 130.116 126.502C130.116 125.22 130.726 124.433 131.709 124.433C132.693 124.433 133.29 125.214 133.29 126.496C133.29 127.791 132.7 128.565 131.703 128.565ZM136.704 130H138.551V125.982C138.551 125.074 139.084 124.458 140.011 124.458C140.849 124.458 141.3 124.96 141.3 125.931V130H143.147V125.493C143.147 123.811 142.252 122.865 140.747 122.865C139.719 122.865 138.957 123.348 138.634 124.147H138.519V120.326H136.704V130ZM147.995 130.152C150.147 130.152 151.442 128.788 151.442 126.496C151.442 124.224 150.128 122.846 147.995 122.846C145.862 122.846 144.548 124.23 144.548 126.496C144.548 128.788 145.843 130.152 147.995 130.152ZM147.995 128.642C147.005 128.642 146.446 127.861 146.446 126.496C146.446 125.15 147.011 124.357 147.995 124.357C148.973 124.357 149.544 125.15 149.544 126.496C149.544 127.854 148.979 128.642 147.995 128.642ZM152.901 130H154.748V125.963C154.748 125.042 155.275 124.439 156.132 124.439C157.008 124.439 157.42 124.947 157.42 125.912V130H159.267V125.474C159.267 123.798 158.429 122.846 156.874 122.846C155.84 122.846 155.129 123.335 154.805 124.122H154.691V122.999H152.901V130ZM164.04 124.262C164.903 124.262 165.461 124.839 165.5 125.766H162.516C162.58 124.858 163.183 124.262 164.04 124.262ZM165.538 128.007C165.341 128.47 164.839 128.73 164.109 128.73C163.145 128.73 162.535 128.083 162.51 127.042V126.947H167.315V126.382C167.315 124.16 166.096 122.846 164.033 122.846C161.951 122.846 160.669 124.255 160.669 126.534C160.669 128.807 161.926 130.152 164.059 130.152C165.772 130.152 166.979 129.327 167.258 128.007H165.538ZM170.113 130.171C170.799 130.171 171.237 129.708 171.237 129.067C171.237 128.426 170.799 127.969 170.113 127.969C169.434 127.969 168.99 128.426 168.99 129.067C168.99 129.708 169.434 130.171 170.113 130.171Z" fill="#272525"/>
-<path d="M23.7968 160V152.494H26.5214V151.545H20.0175V152.494H22.7421V160H23.7968ZM28.032 160H29.0398V156.262C29.0398 155.195 29.6609 154.48 30.7917 154.48C31.7468 154.48 32.2507 155.037 32.2507 156.156V160H33.2585V155.91C33.2585 154.428 32.4148 153.572 31.0788 153.572C30.112 153.572 29.4499 153.982 29.1335 154.68H29.0398V151.176H28.032V160ZM35.0738 160H36.0816V153.684H35.0738V160ZM35.5777 152.4C35.9644 152.4 36.2808 152.084 36.2808 151.697C36.2808 151.311 35.9644 150.994 35.5777 150.994C35.191 150.994 34.8746 151.311 34.8746 151.697C34.8746 152.084 35.191 152.4 35.5777 152.4ZM37.809 155.412C37.809 156.326 38.3481 156.836 39.5317 157.123L40.6157 157.387C41.2895 157.551 41.6176 157.844 41.6176 158.277C41.6176 158.857 41.0082 159.262 40.1586 159.262C39.35 159.262 38.8461 158.922 38.6762 158.389H37.6391C37.7504 159.438 38.7172 160.111 40.1235 160.111C41.559 160.111 42.6547 159.332 42.6547 158.201C42.6547 157.293 42.0805 156.777 40.8911 156.49L39.9184 156.256C39.1743 156.074 38.8227 155.805 38.8227 155.371C38.8227 154.809 39.4086 154.428 40.1586 154.428C40.9203 154.428 41.4125 154.762 41.5473 155.266H42.5434C42.4086 154.229 41.4887 153.572 40.1645 153.572C38.8227 153.572 37.809 154.363 37.809 155.412ZM49.4728 159.227C48.7404 159.227 48.1954 158.852 48.1954 158.207C48.1954 157.574 48.6173 157.24 49.5783 157.176L51.2775 157.064V157.645C51.2775 158.547 50.5099 159.227 49.4728 159.227ZM49.2853 160.111C50.129 160.111 50.8204 159.742 51.2306 159.068H51.3243V160H52.2853V155.676C52.2853 154.363 51.424 153.572 49.8829 153.572C48.5353 153.572 47.5392 154.24 47.4044 155.254H48.424C48.5646 154.756 49.0919 154.469 49.8478 154.469C50.7911 154.469 51.2775 154.896 51.2775 155.676V156.25L49.4552 156.361C47.9845 156.449 47.1525 157.1 47.1525 158.23C47.1525 159.385 48.0607 160.111 49.2853 160.111ZM57.1767 153.572C56.3154 153.572 55.5596 154.012 55.1553 154.738H55.0615V153.684H54.1006V162.109H55.1084V159.051H55.2021C55.5478 159.719 56.2744 160.111 57.1767 160.111C58.7822 160.111 59.831 158.816 59.831 156.842C59.831 154.855 58.7881 153.572 57.1767 153.572ZM56.9365 159.203C55.7998 159.203 55.0791 158.289 55.0791 156.842C55.0791 155.389 55.7998 154.48 56.9424 154.48C58.0967 154.48 58.7881 155.365 58.7881 156.842C58.7881 158.318 58.0967 159.203 56.9365 159.203ZM64.4412 153.572C63.5799 153.572 62.8241 154.012 62.4198 154.738H62.326V153.684H61.3651V162.109H62.3729V159.051H62.4666C62.8123 159.719 63.5389 160.111 64.4412 160.111C66.0467 160.111 67.0955 158.816 67.0955 156.842C67.0955 154.855 66.0526 153.572 64.4412 153.572ZM64.201 159.203C63.0643 159.203 62.3436 158.289 62.3436 156.842C62.3436 155.389 63.0643 154.48 64.2069 154.48C65.3612 154.48 66.0526 155.365 66.0526 156.842C66.0526 158.318 65.3612 159.203 64.201 159.203ZM71.9683 160H72.9761V156.086C72.9761 155.195 73.6734 154.551 74.6343 154.551C74.8335 154.551 75.1968 154.586 75.2788 154.609V153.602C75.1499 153.584 74.939 153.572 74.7749 153.572C73.937 153.572 73.2105 154.006 73.023 154.621H72.9292V153.684H71.9683V160ZM78.8343 154.463C79.8363 154.463 80.5043 155.201 80.5277 156.32H77.0472C77.1234 155.201 77.8265 154.463 78.8343 154.463ZM80.4984 158.365C80.2347 158.922 79.684 159.221 78.8695 159.221C77.7972 159.221 77.1 158.43 77.0472 157.182V157.135H81.5883V156.748C81.5883 154.785 80.5511 153.572 78.8461 153.572C77.1117 153.572 75.9984 154.861 75.9984 156.848C75.9984 158.846 77.0941 160.111 78.8461 160.111C80.2289 160.111 81.2015 159.449 81.5062 158.365H80.4984ZM85.4485 153.572C83.8488 153.572 82.8059 154.861 82.8059 156.842C82.8059 158.834 83.8371 160.111 85.4485 160.111C86.3391 160.111 87.0188 159.742 87.4231 159.051H87.5168V162.109H88.5363V153.684H87.5637V154.738H87.4699C87.0949 154.029 86.3098 153.572 85.4485 153.572ZM85.677 159.203C84.5285 159.203 83.8488 158.324 83.8488 156.842C83.8488 155.365 84.5344 154.48 85.6828 154.48C86.8254 154.48 87.5461 155.395 87.5461 156.842C87.5461 158.295 86.8313 159.203 85.677 159.203ZM95.4962 153.684H94.4883V157.422C94.4883 158.529 93.879 159.191 92.7657 159.191C91.7579 159.191 91.336 158.664 91.336 157.527V153.684H90.3282V157.773C90.3282 159.268 91.0665 160.111 92.4844 160.111C93.4512 160.111 94.1251 159.713 94.4415 159.01H94.5352V160H95.4962V153.684ZM97.37 160H98.3778V153.684H97.37V160ZM97.8739 152.4C98.2607 152.4 98.5771 152.084 98.5771 151.697C98.5771 151.311 98.2607 150.994 97.8739 150.994C97.4872 150.994 97.1708 151.311 97.1708 151.697C97.1708 152.084 97.4872 152.4 97.8739 152.4ZM100.252 160H101.26V156.086C101.26 155.195 101.957 154.551 102.918 154.551C103.117 154.551 103.48 154.586 103.562 154.609V153.602C103.433 153.584 103.222 153.572 103.058 153.572C102.22 153.572 101.494 154.006 101.306 154.621H101.213V153.684H100.252V160ZM107.118 154.463C108.12 154.463 108.788 155.201 108.811 156.32H105.331C105.407 155.201 106.11 154.463 107.118 154.463ZM108.782 158.365C108.518 158.922 107.967 159.221 107.153 159.221C106.081 159.221 105.383 158.43 105.331 157.182V157.135H109.872V156.748C109.872 154.785 108.835 153.572 107.13 153.572C105.395 153.572 104.282 154.861 104.282 156.848C104.282 158.846 105.378 160.111 107.13 160.111C108.512 160.111 109.485 159.449 109.79 158.365H108.782ZM111.259 155.412C111.259 156.326 111.798 156.836 112.982 157.123L114.066 157.387C114.74 157.551 115.068 157.844 115.068 158.277C115.068 158.857 114.458 159.262 113.609 159.262C112.8 159.262 112.296 158.922 112.126 158.389H111.089C111.201 159.438 112.167 160.111 113.574 160.111C115.009 160.111 116.105 159.332 116.105 158.201C116.105 157.293 115.531 156.777 114.341 156.49L113.369 156.256C112.624 156.074 112.273 155.805 112.273 155.371C112.273 154.809 112.859 154.428 113.609 154.428C114.371 154.428 114.863 154.762 114.998 155.266H115.994C115.859 154.229 114.939 153.572 113.615 153.572C112.273 153.572 111.259 154.363 111.259 155.412ZM120.978 160H121.986V156.086C121.986 155.195 122.624 154.48 123.45 154.48C124.247 154.48 124.769 154.961 124.769 155.711V160H125.777V155.939C125.777 155.137 126.362 154.48 127.241 154.48C128.132 154.48 128.571 154.938 128.571 155.869V160H129.579V155.635C129.579 154.311 128.859 153.572 127.569 153.572C126.696 153.572 125.976 154.012 125.636 154.68H125.542C125.249 154.023 124.652 153.572 123.796 153.572C122.952 153.572 122.319 153.977 122.032 154.68H121.939V153.684H120.978V160ZM131.395 160H132.402V153.684H131.395V160ZM131.898 152.4C132.285 152.4 132.602 152.084 132.602 151.697C132.602 151.311 132.285 150.994 131.898 150.994C131.512 150.994 131.195 151.311 131.195 151.697C131.195 152.084 131.512 152.4 131.898 152.4ZM139.503 155.617C139.327 154.492 138.39 153.572 136.854 153.572C135.085 153.572 133.96 154.85 133.96 156.818C133.96 158.828 135.091 160.111 136.86 160.111C138.378 160.111 139.321 159.256 139.503 158.096H138.483C138.296 158.811 137.704 159.203 136.854 159.203C135.729 159.203 135.003 158.277 135.003 156.818C135.003 155.389 135.718 154.48 136.854 154.48C137.763 154.48 138.319 154.99 138.483 155.617H139.503ZM140.943 160H141.951V156.086C141.951 155.195 142.648 154.551 143.609 154.551C143.808 154.551 144.172 154.586 144.254 154.609V153.602C144.125 153.584 143.914 153.572 143.75 153.572C142.912 153.572 142.185 154.006 141.998 154.621H141.904V153.684H140.943V160ZM147.885 160.111C149.684 160.111 150.797 158.869 150.797 156.842C150.797 154.809 149.684 153.572 147.885 153.572C146.086 153.572 144.973 154.809 144.973 156.842C144.973 158.869 146.086 160.111 147.885 160.111ZM147.885 159.203C146.69 159.203 146.016 158.336 146.016 156.842C146.016 155.342 146.69 154.48 147.885 154.48C149.081 154.48 149.754 155.342 149.754 156.842C149.754 158.336 149.081 159.203 147.885 159.203ZM155.408 153.572C154.546 153.572 153.79 154.012 153.386 154.738H153.292V153.684H152.331V162.109H153.339V159.051H153.433C153.779 159.719 154.505 160.111 155.408 160.111C157.013 160.111 158.062 158.816 158.062 156.842C158.062 154.855 157.019 153.572 155.408 153.572ZM155.167 159.203C154.031 159.203 153.31 158.289 153.31 156.842C153.31 155.389 154.031 154.48 155.173 154.48C156.328 154.48 157.019 155.365 157.019 156.842C157.019 158.318 156.328 159.203 155.167 159.203ZM159.655 160H160.662V156.262C160.662 155.195 161.283 154.48 162.414 154.48C163.369 154.48 163.873 155.037 163.873 156.156V160H164.881V155.91C164.881 154.428 164.037 153.572 162.701 153.572C161.735 153.572 161.073 153.982 160.756 154.68H160.662V151.176H159.655V160ZM169.269 160.111C171.067 160.111 172.181 158.869 172.181 156.842C172.181 154.809 171.067 153.572 169.269 153.572C167.47 153.572 166.357 154.809 166.357 156.842C166.357 158.869 167.47 160.111 169.269 160.111ZM169.269 159.203C168.073 159.203 167.4 158.336 167.4 156.842C167.4 155.342 168.073 154.48 169.269 154.48C170.464 154.48 171.138 155.342 171.138 156.842C171.138 158.336 170.464 159.203 169.269 159.203ZM173.715 160H174.723V156.262C174.723 155.154 175.373 154.48 176.381 154.48C177.389 154.48 177.869 155.02 177.869 156.156V160H178.877V155.91C178.877 154.41 178.086 153.572 176.668 153.572C175.701 153.572 175.086 153.982 174.769 154.68H174.676V153.684H173.715V160ZM183.194 154.463C184.196 154.463 184.864 155.201 184.887 156.32H181.407C181.483 155.201 182.186 154.463 183.194 154.463ZM184.858 158.365C184.595 158.922 184.044 159.221 183.229 159.221C182.157 159.221 181.46 158.43 181.407 157.182V157.135H185.948V156.748C185.948 154.785 184.911 153.572 183.206 153.572C181.471 153.572 180.358 154.861 180.358 156.848C180.358 158.846 181.454 160.111 183.206 160.111C184.589 160.111 185.561 159.449 185.866 158.365H184.858ZM192.766 159.227C192.034 159.227 191.489 158.852 191.489 158.207C191.489 157.574 191.911 157.24 192.872 157.176L194.571 157.064V157.645C194.571 158.547 193.803 159.227 192.766 159.227ZM192.579 160.111C193.422 160.111 194.114 159.742 194.524 159.068H194.618V160H195.579V155.676C195.579 154.363 194.717 153.572 193.176 153.572C191.829 153.572 190.833 154.24 190.698 155.254H191.717C191.858 154.756 192.385 154.469 193.141 154.469C194.084 154.469 194.571 154.896 194.571 155.676V156.25L192.749 156.361C191.278 156.449 190.446 157.1 190.446 158.23C190.446 159.385 191.354 160.111 192.579 160.111ZM202.62 155.617C202.445 154.492 201.507 153.572 199.972 153.572C198.202 153.572 197.077 154.85 197.077 156.818C197.077 158.828 198.208 160.111 199.978 160.111C201.495 160.111 202.439 159.256 202.62 158.096H201.601C201.413 158.811 200.822 159.203 199.972 159.203C198.847 159.203 198.12 158.277 198.12 156.818C198.12 155.389 198.835 154.48 199.972 154.48C200.88 154.48 201.437 154.99 201.601 155.617H202.62ZM209.287 155.617C209.111 154.492 208.174 153.572 206.639 153.572C204.869 153.572 203.744 154.85 203.744 156.818C203.744 158.828 204.875 160.111 206.645 160.111C208.162 160.111 209.106 159.256 209.287 158.096H208.268C208.08 158.811 207.488 159.203 206.639 159.203C205.514 159.203 204.787 158.277 204.787 156.818C204.787 155.389 205.502 154.48 206.639 154.48C207.547 154.48 208.104 154.99 208.268 155.617H209.287ZM213.247 154.463C214.249 154.463 214.917 155.201 214.94 156.32H211.46C211.536 155.201 212.239 154.463 213.247 154.463ZM214.911 158.365C214.647 158.922 214.097 159.221 213.282 159.221C212.21 159.221 211.513 158.43 211.46 157.182V157.135H216.001V156.748C216.001 154.785 214.964 153.572 213.259 153.572C211.524 153.572 210.411 154.861 210.411 156.848C210.411 158.846 211.507 160.111 213.259 160.111C214.642 160.111 215.614 159.449 215.919 158.365H214.911ZM217.389 155.412C217.389 156.326 217.928 156.836 219.111 157.123L220.195 157.387C220.869 157.551 221.197 157.844 221.197 158.277C221.197 158.857 220.588 159.262 219.738 159.262C218.93 159.262 218.426 158.922 218.256 158.389H217.219C217.33 159.438 218.297 160.111 219.703 160.111C221.139 160.111 222.234 159.332 222.234 158.201C222.234 157.293 221.66 156.777 220.471 156.49L219.498 156.256C218.754 156.074 218.402 155.805 218.402 155.371C218.402 154.809 218.988 154.428 219.738 154.428C220.5 154.428 220.992 154.762 221.127 155.266H222.123C221.988 154.229 221.068 153.572 219.744 153.572C218.402 153.572 217.389 154.363 217.389 155.412ZM223.505 155.412C223.505 156.326 224.044 156.836 225.227 157.123L226.311 157.387C226.985 157.551 227.313 157.844 227.313 158.277C227.313 158.857 226.704 159.262 225.854 159.262C225.046 159.262 224.542 158.922 224.372 158.389H223.335C223.446 159.438 224.413 160.111 225.819 160.111C227.255 160.111 228.35 159.332 228.35 158.201C228.35 157.293 227.776 156.777 226.587 156.49L225.614 156.256C224.87 156.074 224.518 155.805 224.518 155.371C224.518 154.809 225.104 154.428 225.854 154.428C226.616 154.428 227.108 154.762 227.243 155.266H228.239C228.104 154.229 227.184 153.572 225.86 153.572C224.518 153.572 223.505 154.363 223.505 155.412ZM233.645 152.049V153.684H232.625V154.527H233.645V158.359C233.645 159.566 234.166 160.047 235.467 160.047C235.666 160.047 235.86 160.023 236.059 159.988V159.139C235.872 159.156 235.772 159.162 235.59 159.162C234.934 159.162 234.653 158.846 234.653 158.102V154.527H236.059V153.684H234.653V152.049H233.645ZM240.036 160.111C241.835 160.111 242.949 158.869 242.949 156.842C242.949 154.809 241.835 153.572 240.036 153.572C238.238 153.572 237.124 154.809 237.124 156.842C237.124 158.869 238.238 160.111 240.036 160.111ZM240.036 159.203C238.841 159.203 238.167 158.336 238.167 156.842C238.167 155.342 238.841 154.48 240.036 154.48C241.232 154.48 241.906 155.342 241.906 156.842C241.906 158.336 241.232 159.203 240.036 159.203ZM27.91 173.227C27.1776 173.227 26.6327 172.852 26.6327 172.207C26.6327 171.574 27.0546 171.24 28.0155 171.176L29.7147 171.064V171.645C29.7147 172.547 28.9472 173.227 27.91 173.227ZM27.7225 174.111C28.5663 174.111 29.2577 173.742 29.6679 173.068H29.7616V174H30.7225V169.676C30.7225 168.363 29.8612 167.572 28.3202 167.572C26.9725 167.572 25.9765 168.24 25.8417 169.254H26.8612C27.0018 168.756 27.5292 168.469 28.285 168.469C29.2284 168.469 29.7147 168.896 29.7147 169.676V170.25L27.8925 170.361C26.4218 170.449 25.5897 171.1 25.5897 172.23C25.5897 173.385 26.4979 174.111 27.7225 174.111ZM32.5964 174H33.6042V165.176H32.5964V174ZM35.5719 174H36.5797V165.176H35.5719V174ZM41.0844 174.111C42.8832 174.111 43.9965 172.869 43.9965 170.842C43.9965 168.809 42.8832 167.572 41.0844 167.572C39.2856 167.572 38.1723 168.809 38.1723 170.842C38.1723 172.869 39.2856 174.111 41.0844 174.111ZM41.0844 173.203C39.8891 173.203 39.2153 172.336 39.2153 170.842C39.2153 169.342 39.8891 168.48 41.0844 168.48C42.2797 168.48 42.9535 169.342 42.9535 170.842C42.9535 172.336 42.2797 173.203 41.0844 173.203ZM53.2415 167.684H52.2278L50.9856 172.734H50.8919L49.4798 167.684H48.513L47.1009 172.734H47.0071L45.7649 167.684H44.7454L46.5149 174H47.5345L48.9407 169.113H49.0345L50.4466 174H51.472L53.2415 167.684ZM58.3017 166.049V167.684H57.2822V168.527H58.3017V172.359C58.3017 173.566 58.8232 174.047 60.124 174.047C60.3232 174.047 60.5166 174.023 60.7158 173.988V173.139C60.5283 173.156 60.4287 173.162 60.247 173.162C59.5908 173.162 59.3095 172.846 59.3095 172.102V168.527H60.7158V167.684H59.3095V166.049H58.3017ZM62.2498 174H63.2576V170.262C63.2576 169.195 63.8787 168.48 65.0096 168.48C65.9647 168.48 66.4686 169.037 66.4686 170.156V174H67.4764V169.91C67.4764 168.428 66.6326 167.572 65.2967 167.572C64.3299 167.572 63.6678 167.982 63.3514 168.68H63.2576V165.176H62.2498V174ZM71.7878 168.463C72.7897 168.463 73.4577 169.201 73.4811 170.32H70.0007C70.0768 169.201 70.78 168.463 71.7878 168.463ZM73.4518 172.365C73.1882 172.922 72.6374 173.221 71.8229 173.221C70.7507 173.221 70.0534 172.43 70.0007 171.182V171.135H74.5417V170.748C74.5417 168.785 73.5046 167.572 71.7995 167.572C70.0651 167.572 68.9518 168.861 68.9518 170.848C68.9518 172.846 70.0475 174.111 71.7995 174.111C73.1823 174.111 74.155 173.449 74.4596 172.365H73.4518ZM80.4457 167.684H79.4379V174.328C79.4379 175.025 79.2035 175.307 78.4886 175.307H78.3363V176.168H78.5121C79.8363 176.168 80.4457 175.611 80.4457 174.311V167.684ZM79.9418 166.4C80.3285 166.4 80.6449 166.084 80.6449 165.697C80.6449 165.311 80.3285 164.994 79.9418 164.994C79.555 164.994 79.2386 165.311 79.2386 165.697C79.2386 166.084 79.555 166.4 79.9418 166.4ZM84.2649 173.227C83.5324 173.227 82.9875 172.852 82.9875 172.207C82.9875 171.574 83.4094 171.24 84.3703 171.176L86.0695 171.064V171.645C86.0695 172.547 85.302 173.227 84.2649 173.227ZM84.0774 174.111C84.9211 174.111 85.6125 173.742 86.0227 173.068H86.1164V174H87.0774V169.676C87.0774 168.363 86.216 167.572 84.675 167.572C83.3274 167.572 82.3313 168.24 82.1965 169.254H83.216C83.3567 168.756 83.884 168.469 84.6399 168.469C85.5832 168.469 86.0695 168.896 86.0695 169.676V170.25L84.2473 170.361C82.7766 170.449 81.9445 171.1 81.9445 172.23C81.9445 173.385 82.8528 174.111 84.0774 174.111ZM94.1192 169.617C93.9434 168.492 93.0059 167.572 91.4708 167.572C89.7012 167.572 88.5762 168.85 88.5762 170.818C88.5762 172.828 89.7071 174.111 91.4766 174.111C92.9942 174.111 93.9376 173.256 94.1192 172.096H93.0997C92.9122 172.811 92.3204 173.203 91.4708 173.203C90.3458 173.203 89.6192 172.277 89.6192 170.818C89.6192 169.389 90.334 168.48 91.4708 168.48C92.379 168.48 92.9356 168.99 93.0997 169.617H94.1192ZM96.7196 170.443H96.6259V165.176H95.6181V174H96.6259V171.604L97.2294 171.041L99.5966 174H100.88L97.9794 170.391L100.698 167.684H99.4618L96.7196 170.443ZM105.219 169.412C105.219 170.326 105.758 170.836 106.942 171.123L108.026 171.387C108.7 171.551 109.028 171.844 109.028 172.277C109.028 172.857 108.419 173.262 107.569 173.262C106.76 173.262 106.256 172.922 106.087 172.389H105.049C105.161 173.438 106.128 174.111 107.534 174.111C108.969 174.111 110.065 173.332 110.065 172.201C110.065 171.293 109.491 170.777 108.301 170.49L107.329 170.256C106.585 170.074 106.233 169.805 106.233 169.371C106.233 168.809 106.819 168.428 107.569 168.428C108.331 168.428 108.823 168.762 108.958 169.266H109.954C109.819 168.229 108.899 167.572 107.575 167.572C106.233 167.572 105.219 168.363 105.219 169.412ZM114.119 168.463C115.121 168.463 115.789 169.201 115.812 170.32H112.332C112.408 169.201 113.111 168.463 114.119 168.463ZM115.783 172.365C115.519 172.922 114.968 173.221 114.154 173.221C113.082 173.221 112.384 172.43 112.332 171.182V171.135H116.873V170.748C116.873 168.785 115.835 167.572 114.13 167.572C112.396 167.572 111.283 168.861 111.283 170.848C111.283 172.846 112.378 174.111 114.13 174.111C115.513 174.111 116.486 173.449 116.79 172.365H115.783ZM118.407 174H119.414V170.086C119.414 169.195 120.112 168.551 121.073 168.551C121.272 168.551 121.635 168.586 121.717 168.609V167.602C121.588 167.584 121.377 167.572 121.213 167.572C120.375 167.572 119.649 168.006 119.461 168.621H119.367V167.684H118.407V174ZM128.085 167.684H127.007L125.278 172.887H125.185L123.456 167.684H122.378L124.716 174H125.747L128.085 167.684ZM131.67 168.463C132.672 168.463 133.34 169.201 133.363 170.32H129.883C129.959 169.201 130.662 168.463 131.67 168.463ZM133.334 172.365C133.07 172.922 132.52 173.221 131.705 173.221C130.633 173.221 129.936 172.43 129.883 171.182V171.135H134.424V170.748C134.424 168.785 133.387 167.572 131.682 167.572C129.947 167.572 128.834 168.861 128.834 170.848C128.834 172.846 129.93 174.111 131.682 174.111C133.064 174.111 134.037 173.449 134.342 172.365H133.334ZM135.958 174H136.966V170.086C136.966 169.195 137.663 168.551 138.624 168.551C138.823 168.551 139.186 168.586 139.268 168.609V167.602C139.14 167.584 138.929 167.572 138.765 167.572C137.927 167.572 137.2 168.006 137.013 168.621H136.919V167.684H135.958V174ZM144.241 166.049V167.684H143.221V168.527H144.241V172.359C144.241 173.566 144.762 174.047 146.063 174.047C146.262 174.047 146.456 174.023 146.655 173.988V173.139C146.467 173.156 146.368 173.162 146.186 173.162C145.53 173.162 145.249 172.846 145.249 172.102V168.527H146.655V167.684H145.249V166.049H144.241ZM150.632 174.111C152.431 174.111 153.544 172.869 153.544 170.842C153.544 168.809 152.431 167.572 150.632 167.572C148.833 167.572 147.72 168.809 147.72 170.842C147.72 172.869 148.833 174.111 150.632 174.111ZM150.632 173.203C149.437 173.203 148.763 172.336 148.763 170.842C148.763 169.342 149.437 168.48 150.632 168.48C151.828 168.48 152.501 169.342 152.501 170.842C152.501 172.336 151.828 173.203 150.632 173.203ZM163.644 169.617C163.468 168.492 162.53 167.572 160.995 167.572C159.226 167.572 158.101 168.85 158.101 170.818C158.101 172.828 159.232 174.111 161.001 174.111C162.519 174.111 163.462 173.256 163.644 172.096H162.624C162.437 172.811 161.845 173.203 160.995 173.203C159.87 173.203 159.144 172.277 159.144 170.818C159.144 169.389 159.858 168.48 160.995 168.48C161.903 168.48 162.46 168.99 162.624 169.617H163.644ZM167.029 173.227C166.297 173.227 165.752 172.852 165.752 172.207C165.752 171.574 166.174 171.24 167.135 171.176L168.834 171.064V171.645C168.834 172.547 168.066 173.227 167.029 173.227ZM166.842 174.111C167.685 174.111 168.377 173.742 168.787 173.068H168.881V174H169.842V169.676C169.842 168.363 168.98 167.572 167.439 167.572C166.092 167.572 165.096 168.24 164.961 169.254H165.98C166.121 168.756 166.648 168.469 167.404 168.469C168.348 168.469 168.834 168.896 168.834 169.676V170.25L167.012 170.361C165.541 170.449 164.709 171.1 164.709 172.23C164.709 173.385 165.617 174.111 166.842 174.111ZM174.733 167.572C173.872 167.572 173.116 168.012 172.712 168.738H172.618V167.684H171.657V176.109H172.665V173.051H172.759C173.104 173.719 173.831 174.111 174.733 174.111C176.339 174.111 177.387 172.816 177.387 170.842C177.387 168.855 176.345 167.572 174.733 167.572ZM174.493 173.203C173.356 173.203 172.636 172.289 172.636 170.842C172.636 169.389 173.356 168.48 174.499 168.48C175.653 168.48 176.345 169.365 176.345 170.842C176.345 172.318 175.653 173.203 174.493 173.203ZM179.343 166.049V167.684H178.324V168.527H179.343V172.359C179.343 173.566 179.865 174.047 181.166 174.047C181.365 174.047 181.558 174.023 181.757 173.988V173.139C181.57 173.156 181.47 173.162 181.289 173.162C180.632 173.162 180.351 172.846 180.351 172.102V168.527H181.757V167.684H180.351V166.049H179.343ZM188.342 167.684H187.334V171.422C187.334 172.529 186.725 173.191 185.612 173.191C184.604 173.191 184.182 172.664 184.182 171.527V167.684H183.174V171.773C183.174 173.268 183.913 174.111 185.331 174.111C186.297 174.111 186.971 173.713 187.288 173.01H187.381V174H188.342V167.684ZM190.193 174H191.201V170.086C191.201 169.195 191.898 168.551 192.859 168.551C193.058 168.551 193.421 168.586 193.503 168.609V167.602C193.374 167.584 193.163 167.572 192.999 167.572C192.161 167.572 191.435 168.006 191.247 168.621H191.154V167.684H190.193V174ZM197.059 168.463C198.061 168.463 198.729 169.201 198.752 170.32H195.272C195.348 169.201 196.051 168.463 197.059 168.463ZM198.723 172.365C198.459 172.922 197.908 173.221 197.094 173.221C196.022 173.221 195.324 172.43 195.272 171.182V171.135H199.813V170.748C199.813 168.785 198.776 167.572 197.07 167.572C195.336 167.572 194.223 168.861 194.223 170.848C194.223 172.846 195.319 174.111 197.07 174.111C198.453 174.111 199.426 173.449 199.731 172.365H198.723ZM206.631 173.227C205.898 173.227 205.353 172.852 205.353 172.207C205.353 171.574 205.775 171.24 206.736 171.176L208.435 171.064V171.645C208.435 172.547 207.668 173.227 206.631 173.227ZM206.443 174.111C207.287 174.111 207.978 173.742 208.389 173.068H208.482V174H209.443V169.676C209.443 168.363 208.582 167.572 207.041 167.572C205.693 167.572 204.697 168.24 204.562 169.254H205.582C205.723 168.756 206.25 168.469 207.006 168.469C207.949 168.469 208.435 168.896 208.435 169.676V170.25L206.613 170.361C205.142 170.449 204.31 171.1 204.31 172.23C204.31 173.385 205.219 174.111 206.443 174.111ZM216.368 167.684H215.36V171.422C215.36 172.529 214.751 173.191 213.637 173.191C212.63 173.191 212.208 172.664 212.208 171.527V167.684H211.2V171.773C211.2 173.268 211.938 174.111 213.356 174.111C214.323 174.111 214.997 173.713 215.313 173.01H215.407V174H216.368V167.684ZM220.556 174.111C221.429 174.111 222.179 173.695 222.578 172.992H222.671V174H223.632V165.176H222.625V168.68H222.537C222.179 167.988 221.435 167.572 220.556 167.572C218.951 167.572 217.902 168.861 217.902 170.842C217.902 172.828 218.939 174.111 220.556 174.111ZM220.791 168.48C221.933 168.48 222.648 169.395 222.648 170.842C222.648 172.301 221.939 173.203 220.791 173.203C219.636 173.203 218.945 172.318 218.945 170.842C218.945 169.371 219.642 168.48 220.791 168.48ZM225.565 174H226.573V167.684H225.565V174ZM226.069 166.4C226.455 166.4 226.772 166.084 226.772 165.697C226.772 165.311 226.455 164.994 226.069 164.994C225.682 164.994 225.366 165.311 225.366 165.697C225.366 166.084 225.682 166.4 226.069 166.4ZM231.042 174.111C232.841 174.111 233.954 172.869 233.954 170.842C233.954 168.809 232.841 167.572 231.042 167.572C229.243 167.572 228.13 168.809 228.13 170.842C228.13 172.869 229.243 174.111 231.042 174.111ZM231.042 173.203C229.847 173.203 229.173 172.336 229.173 170.842C229.173 169.342 229.847 168.48 231.042 168.48C232.238 168.48 232.911 169.342 232.911 170.842C232.911 172.336 232.238 173.203 231.042 173.203ZM236.203 174.059C236.625 174.059 236.965 173.713 236.965 173.297C236.965 172.875 236.625 172.535 236.203 172.535C235.787 172.535 235.442 172.875 235.442 173.297C235.442 173.713 235.787 174.059 236.203 174.059Z" fill="#272525"/>
-</svg>
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property int fontBig: 20
- property int fontMedium: 13
- property int fontSmall: 11
- property int fontExtraSmall: 8
-
- property int buttonWidth: 103
- property int buttonHeight: 25
-
- property int leftMargin: 48
- property int rightMargin: 16
-
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string saveButtonBackgroundColour: "#F2F3F3"
- property string saveButtonPressedColour: "#E7E8E8"
- property string saveButtonStroke: "#EAEBEB"
- property string saveButtonPressedStroke: "#B0B5B5"
- property string recommendationText: "#DB0A0A"
- property string saveButtonText: "#DB0A0A"
- property string checkboxStroke: "#0062cc"
- property string checkboxPressedStroke: "#007AFF"
- property string disabledButtonText: "#D3D4D4"
- property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
-
- property bool currShowRecommendations: virtualstudio.showWarnings
- property string recommendationScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged")
- property bool onWindows: Qt.platform.os === "windows"
-
- Rectangle {
- id: recommendationsHeader
- x: -1
- y: 0
-
- width: parent.width + 2
- height: 64
-
- color: backgroundColour
- border.color: "#33979797"
-
- Image {
- source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
- anchors.left: parent.left
- anchors.leftMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- width: 119 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
- }
-
- Text {
- id: gettingStartedText1
- visible: recommendationScreen === "ethernet"
- text: "Getting Started with JackTrip (1/5)"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- }
-
- Text {
- id: gettingStartedText2
- visible: recommendationScreen === "fiber"
- text: "Getting Started with JackTrip (2/5)"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- }
-
- Text {
- id: gettingStartedText3
- visible: recommendationScreen === "audiointerface"
- text: "Getting Started with JackTrip (3/5)"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- }
-
- Text {
- id: gettingStartedText4
- visible: recommendationScreen === "headphones"
- text: "Getting Started with JackTrip (4/5)"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- }
-
- Text {
- id: gettingStartedText5
- visible: recommendationScreen === "acknowledged"
- text: "Getting Started with JackTrip (5/5)"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.right: parent.right
- anchors.rightMargin: 32 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- Item {
- id: ethernetRecommendationItem
- width: parent.width; height: parent.height
- visible: recommendationScreen == "ethernet"
-
- AppIcon {
- id: ethernetRecommendationLogo
- y: 90
- anchors.horizontalCenter: parent.horizontalCenter
- width: 179
- height: 128
- icon.source: "ethernet.svg"
- }
-
- Text {
- id: ethernetRecommendationHeader1
- text: "Wired Ethernet Recommended"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ethernetRecommendationLogo.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: ethernetRecommendationSubheader1
- text: "JackTrip works best when you connect your computer directly to your Internet router via a wired ethernet cable."
- + "<br/><br/>"
- + "WiFi works OK for some people, but generates significantly more latency and audio glitches."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ethernetRecommendationHeader1.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- LearnMoreButton {
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: ethernetRecommendationSubheader1.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- url: "https://support.jacktrip.com/wired-internet-versus-wi-fi"
- }
-
- Button {
- id: okButtonEthernet
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down
- }
- onClicked: { recommendationScreen = "fiber" }
- anchors.right: parent.right
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Continue"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
-
- Item {
- id: fiberRecommendationItem
- width: parent.width; height: parent.height
- visible: recommendationScreen == "fiber"
-
- AppIcon {
- id: fiberRecommendationLogo
- y: 90
- anchors.horizontalCenter: parent.horizontalCenter
- width: 179
- height: 128
- icon.source: "networkCheck.svg"
- }
-
- Text {
- id: fiberRecommendationHeader
- text: "Fiber Internet Recommended"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: fiberRecommendationLogo.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: fiberRecommendationSubheader
- text: "A Fiber Internet connection from your Internet Service Provider (ISP) will give you the best experience while using JackTrip."
- + "<br/><br/>"
- + "It's OK to use JackTrip with Cable and DSL, but these types of Internet connections introduce significantly more latency."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: fiberRecommendationHeader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- LearnMoreButton {
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: fiberRecommendationSubheader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip"
- }
-
- Button {
- id: okButtonFiber
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: okButtonFiber.hovered && !okButtonFiber.down
- }
- onClicked: { recommendationScreen = "audiointerface" }
- anchors.right: parent.right
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Continue"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
-
- Item {
- id: headphoneRecommendationItem
- width: parent.width; height: parent.height
- visible: recommendationScreen == "headphones"
-
- AppIcon {
- id: headphoneWarningLogo
- y: 90
- anchors.horizontalCenter: parent.horizontalCenter
- width: 118
- height: 128
- icon.source: "headphones.svg"
- }
-
- Text {
- id: headphoneRecommendationHeader1
- text: "Wired Headphones Required"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: headphoneWarningLogo.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: headphoneRecommendationSubheader1
- text: "JackTrip requires the use of wired headphones."
- + "<br/><br/>"
- + "Using speakers will generate echos and loud feedback loops."
- + "<br/><br/>"
- + "Wireless and bluetooth headphones introduce higher latency."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: headphoneRecommendationHeader1.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Button {
- id: okButtonHeadphones
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down
- }
- onClicked: {
- recommendationScreen = "acknowledged";
- }
- anchors.right: parent.right
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Continue"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
-
- Item {
- id: audioInterfaceRecommendationItem
- width: parent.width; height: parent.height
- visible: recommendationScreen == "audiointerface"
-
- AppIcon {
- id: audioInterfaceRecommendationLogo
- y: 90
- anchors.horizontalCenter: parent.horizontalCenter
- width: 118
- height: 128
- icon.source: "externalMic.svg"
- }
-
- Text {
- id: audioInterfaceRecommendationHeaderNonWindows
- visible: !onWindows
- text: "Use Recommended Audio Devices"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: audioInterfaceRecommendationLogo.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: audioInterfaceRecommendationSubheaderNonWindows
- visible: !onWindows
- text: "Many audio devices are too slow to work well with JackTrip."
- + "<br/><br/>"
- + "We recommend external USB or Thunderbolt interfaces released within the past "
- + "few years for the best quality, low latency and glitch-free sound."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: audioInterfaceRecommendationHeaderNonWindows.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: audioInterfaceRecommendationHeaderWindows
- visible: onWindows
- text: "Use Recommended Audio Devices"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: audioInterfaceRecommendationLogo.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Text {
- id: audioInterfaceRecommendationSubheaderWindows
- visible: onWindows
- text: "Many audio devices are too slow to work well with JackTrip."
- + "<br/>"
- + "ASIO drivers are required for low latency on Windows. "
- + "<br/><br/>"
- + "We recommend external USB or Thunderbolt interfaces released within the past "
- + "few years for the best quality, low latency and glitch-free sound."
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: audioInterfaceRecommendationHeaderWindows.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- LearnMoreButton {
- width: 250 * virtualstudio.uiScale;
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: onWindows ? audioInterfaceRecommendationSubheaderWindows.bottom : audioInterfaceRecommendationSubheaderNonWindows.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- buttonText: "See recommended devices"
- url: "https://support.jacktrip.com/recommended-audio-interfaces"
- }
-
- Button {
- id: okButtonAudioInterface
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down
- }
- onClicked: {
- recommendationScreen = "headphones";
- }
- anchors.right: parent.right
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: 16 * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Continue"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
-
- Item {
- id: acknowledgedRecommendationItem
- width: parent.width; height: parent.height
- visible: recommendationScreen == "acknowledged"
-
- Text {
- id: acknowledgedHeader
- text: "Remind Me Again Next Time?"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: parent.top
- anchors.topMargin: 176 * virtualstudio.uiScale
- }
-
- Text {
- id: acknowledgedSubheader
- text: "Would you like to review the getting started recommendations again the next time you start JackTrip?"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: acknowledgedHeader.bottom
- anchors.topMargin: 32 * virtualstudio.uiScale
- }
-
- Item {
- id: acknowledgedButtonsContainer
- width: 320 * virtualstudio.uiScale
-
- anchors.top: acknowledgedSubheader.bottom
- anchors.topMargin: 64 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
-
- Button {
- id: acknowledgedYesButton
- anchors.left: parent.left
- anchors.verticalCenter: parent.verticalCenter
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down
- }
- onClicked: {
- virtualstudio.showWarnings = true;
- virtualstudio.saveSettings();
- if (permissions.micPermission !== "granted") {
- virtualstudio.windowState = "permissions";
- } else if (virtualstudio.studioToJoin === "") {
- virtualstudio.windowState = "browse";
- } else {
- virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
- virtualstudio.joinStudio();
- }
- }
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Yes"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- Button {
- id: acknowledgedNoButton
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke
- layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down
- }
- onClicked: {
- virtualstudio.showWarnings = false;
- virtualstudio.saveSettings();
- if (permissions.micPermission !== "granted") {
- virtualstudio.windowState = "permissions";
- } else if (virtualstudio.studioToJoin === "") {
- virtualstudio.windowState = "browse";
- } else {
- virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
- virtualstudio.joinStudio();
- }
- }
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "No"
- font.family: "Poppins"
- font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: saveButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
- }
-
-
- Text {
- id: acknowledgedSettingsInfo
- text: "You can change this setting at any time under <b>Settings > Advanced</b>"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- width: 600
- wrapMode: Text.Wrap
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: acknowledgedButtonsContainer.bottom
- anchors.topMargin: 64 * virtualstudio.uiScale
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Rectangle {
- property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-
- 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
- }
- 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
- }
- 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: filterStroke
- layer.enabled: true
- }
- 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
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- Rectangle {
- width: parent.width; height: parent.height
- color: backgroundColour
- }
-
- property int fontBig: 20
- property int fontMedium: 13
- property int fontSmall: 11
- property int fontExtraSmall: 8
-
- property int leftMargin: 48
- property int rightMargin: 16
- property int buttonWidth: 103
- property int buttonHeight: 25
-
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC"
- property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0"
- property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray"
- property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black"
- property string warningTextColour: "#DB0A0A"
- property string checkboxStroke: "#0062cc"
- property string checkboxPressedStroke: "#007AFF"
- property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
-
- property string errorFlagColour: "#DB0A0A"
- property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-
- property string settingsGroupView: "Audio"
-
- function getCurrentBufferSizeIndex () {
- let bufferSize = audio.bufferSize;
- let idx = audio.bufferSizeComboModel.findIndex(elem => parseInt(elem) === bufferSize);
- if (idx < 0) {
- idx = 0;
- }
- return idx;
- }
-
- function getCurrentAudioBackendIndex () {
- let idx = audio.audioBackendComboModel.findIndex(elem => elem === audio.audioBackend);
- if (idx < 0) {
- idx = 0;
- }
- return idx;
- }
-
- Rectangle {
- id: audioSettingsView
- width: 0.8 * parent.width
- height: parent.height - header.height
- x: 0.2 * window.width
- y: header.height
- visible: settingsGroupView == "Audio"
-
- AudioSettings {
- id: audioSettings
- }
- }
-
- ToolBar {
- id: header
- width: parent.width
- height: 64 * virtualstudio.uiScale
-
- background: Rectangle {
- border.color: "#33979797"
- color: backgroundColour
- width: parent.width
- }
-
- contentItem: Item {
- id: headerContent
- width: header.width
- height: header.height
-
- Label {
- id: pageTitle
- text: "Settings"
- height: headerContent.height;
- anchors.left: headerContent.left;
- anchors.leftMargin: 32 * virtualstudio.uiScale
- elide: Label.ElideRight
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- DeviceRefreshButton {
- id: refreshButton
- anchors.verticalCenter: pageTitle.verticalCenter;
- anchors.right: headerContent.right;
- anchors.rightMargin: 16 * virtualstudio.uiScale;
- visible: audio.audioBackend == "RtAudio" && settingsGroupView == "Audio"
- enabled: audio.audioReady && !audio.scanningDevices
- }
-
- Text {
- text: "Restarting Audio"
- anchors.verticalCenter: pageTitle.verticalCenter;
- anchors.right: refreshButton.left;
- anchors.rightMargin: 16 * virtualstudio.uiScale;
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- visible: !audio.audioReady
- }
- }
- }
-
- Drawer {
- id: drawer
- width: 0.2 * parent.width
- height: parent.height - header.height
- y: header.height-1
- modal: false
- interactive: false
- visible: virtualstudio.windowState == "settings"
-
- background: Rectangle {
- border.color: "#33979797"
- color: backgroundColour
- }
-
- ButtonGroup {
- buttons: viewControls.children
- onClicked: function(button) {
- settingsGroupView = button.text
- }
- }
-
- Column {
- id: viewControls
- width: parent.width
- spacing: 24 * virtualstudio.uiScale
- anchors.centerIn: parent
- Button {
- id: audioBtn
- text: "Audio"
- width: parent.width
- contentItem: Item {
- implicitWidth: audioButtonText.implicitWidth
- implicitHeight: audioButtonText.implicitHeight
-
- Label {
- id: audioButtonText
- text: audioBtn.text
- width: Boolean(audio.devicesError) ? parent.width - 16 * virtualstudio.uiScale : parent.width
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- color: textColour
- }
-
- Rectangle {
- id: audioDevicesErrorFlag
- anchors.left: audioButtonText.right
- anchors.verticalCenter: audioButtonText.verticalCenter
- anchors.rightMargin: 16 * virtualstudio.uiScale
- width: 8 * virtualstudio.uiScale
- height: 8 * virtualstudio.uiScale
- color: errorFlagColour
- radius: 4 * virtualstudio.uiScale
- visible: Boolean(audio.devicesError)
- }
- }
- background: Rectangle {
- width: parent.width
- color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour)
- }
- }
- Button {
- id: appearanceBtn
- text: "Appearance"
- width: parent.width
- contentItem: Item {
- implicitWidth: appearanceButtonText.implicitWidth
- implicitHeight: appearanceButtonText.implicitHeight
-
- Label {
- id: appearanceButtonText
- text: appearanceBtn.text
- width: parent.width
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- color: textColour
- }
- }
-
-
- background: Rectangle {
- width: parent.width
- color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour)
- }
- }
- Button {
- id: advancedBtn
- text: "Advanced"
- width: parent.width
- contentItem: Item {
- implicitWidth: advancedButtonText.implicitWidth
- implicitHeight: advancedButtonText.implicitHeight
-
- Label {
- id: advancedButtonText
- text: advancedBtn.text
- width: parent.width
- 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"
- width: parent.width
- contentItem: Item {
-
- implicitWidth: profileButtonText.implicitWidth
- implicitHeight: profileButtonText.implicitHeight
-
- Label {
- id: profileButtonText
- text: profileBtn.text
- width: parent.width
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- color: textColour
- }
- }
- background: Rectangle {
- width: parent.width
- color: profileBtn.down ? buttonPressedColour : (profileBtn.hovered || settingsGroupView == "Profile" ? buttonHoverColour : backgroundColour)
- }
- }
- }
-
- Column {
- id: appVersion
- width: parent.width
- spacing: 24 * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.bottom: parent.bottom
-
- Text {
- text: "Version " + virtualstudio.versionString
- font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale}
- color: textColour
- opacity: 0.8
- width: parent.width
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- bottomPadding: 5 * virtualstudio.uiScale
- }
- }
- }
-
- Rectangle {
- id: appearanceSettingsView
- width: 0.8 * parent.width
- height: parent.height - header.height
- x: 0.2 * window.width
- y: header.height
- color: backgroundColour
- visible: settingsGroupView == "Appearance"
-
- Slider {
- id: scaleSlider
- x: 220 * virtualstudio.uiScale;
- y: 100 * virtualstudio.uiScale
- width: backendCombo.width
- from: 1; to: 1.25; value: virtualstudio.uiScale
- onMoved: { virtualstudio.uiScale = value }
-
- background: Rectangle {
- x: scaleSlider.leftPadding
- y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
- implicitWidth: parent.width
- implicitHeight: 6
- width: scaleSlider.availableWidth
- height: implicitHeight
- radius: 4
- color: sliderTrackColour
-
- Rectangle {
- width: scaleSlider.visualPosition * parent.width
- height: parent.height
- color: sliderActiveTrackColour
- radius: 4
- }
- }
-
- handle: Rectangle {
- x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width)
- y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
- implicitWidth: 26 * virtualstudio.uiScale
- implicitHeight: 26 * virtualstudio.uiScale
- radius: 13 * virtualstudio.uiScale
- color: scaleSlider.pressed ? sliderPressedColour : sliderColour
- border.color: buttonStroke
- }
- }
-
- Text {
- anchors.verticalCenter: scaleSlider.verticalCenter
- x: leftMargin * virtualstudio.uiScale
- text: "Scale Interface"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Button {
- id: darkButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- 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.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
- }
- }
-
- Text {
- anchors.verticalCenter: darkButton.verticalCenter
- x: leftMargin * virtualstudio.uiScale
- 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: modeButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: {
- // essentially the same here as clicking the cancel button
- audio.stopAudio();
- virtualstudio.windowState = "browse";
- virtualstudio.loadSettings();
- audio.validateDevices();
-
- // switch mode
- virtualstudio.toStandard();
- }
- x: 220 * virtualstudio.uiScale;
- y: 100 * virtualstudio.uiScale
- width: 216 * virtualstudio.uiScale;
- height: 30 * virtualstudio.uiScale
- Text {
- 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
- }
- }
-
- Text {
- anchors.verticalCenter: modeButton.verticalCenter
- x: leftMargin * virtualstudio.uiScale
- text: "Display Mode"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: updateChannelCombo
- x: 220 * virtualstudio.uiScale; y: modeButton.y + (48 * virtualstudio.uiScale)
- width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
- model: virtualstudio.updateChannelComboModel
- currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1
- onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" }
- font.family: "Poppins"
- enabled: !virtualstudio.noUpdater
- }
-
- Text {
- anchors.verticalCenter: updateChannelCombo.verticalCenter
- x: leftMargin * virtualstudio.uiScale
- text: "Update Channel"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: backendCombo
- model: audio.audioBackendComboModel
- enabled: audio.audioBackendComboModel.length > 1
- currentIndex: getCurrentAudioBackendIndex()
- onActivated: {
- audio.audioBackend = currentText
- audio.restartAudio();
- }
- x: 220 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale)
- width: updateChannelCombo.width; height: updateChannelCombo.height
- }
-
- Text {
- id: backendLabel
- anchors.verticalCenter: backendCombo.verticalCenter
- x: leftMargin * virtualstudio.uiScale
- text: "Audio Backend"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: bufferCombo
- x: 220 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale)
- width: backendCombo.width; height: updateChannelCombo.height
- model: audio.bufferSizeComboModel
- currentIndex: getCurrentBufferSizeIndex()
- onActivated: {
- audio.bufferSize = parseInt(currentText, 10);
- audio.restartAudio();
- }
- font.family: "Poppins"
- }
-
- Text {
- anchors.verticalCenter: bufferCombo.verticalCenter
- x: 48 * virtualstudio.uiScale
- text: "Buffer Size"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: bufferStrategyCombo
- x: updateChannelCombo.x; y: bufferCombo.y + (48 * virtualstudio.uiScale)
- width: updateChannelCombo.width; height: updateChannelCombo.height
- model: audio.bufferStrategyComboModel
- currentIndex: audio.bufferStrategy
- onActivated: { audio.bufferStrategy = currentIndex }
- font.family: "Poppins"
- }
-
- Text {
- anchors.verticalCenter: bufferStrategyCombo.verticalCenter
- x: 48 * virtualstudio.uiScale
- text: "Buffer Strategy"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- ComboBox {
- id: feedbackDetectionCombo
- x: updateChannelCombo.x; y: bufferStrategyCombo.y + (48 * virtualstudio.uiScale)
- width: updateChannelCombo.width; height: updateChannelCombo.height
- model: audio.feedbackDetectionComboModel
- currentIndex: audio.feedbackDetectionEnabled ? 0 : 1
- onActivated: {
- if (currentIndex === 1) {
- audio.feedbackDetectionEnabled = false;
- } else {
- audio.feedbackDetectionEnabled = true;
- }
- }
- font.family: "Poppins"
- }
-
- Text {
- anchors.verticalCenter: feedbackDetectionCombo.verticalCenter
- x: 48 * virtualstudio.uiScale
- text: "Feedback Detection"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- CheckBox {
- id: showStartupSetup
- checked: virtualstudio.showDeviceSetup
- text: qsTr("Show device setup screen before connecting to a studio")
- x: updateChannelCombo.x; y: feedbackDetectionCombo.y + (48 * virtualstudio.uiScale)
- onClicked: { virtualstudio.showDeviceSetup = showStartupSetup.checkState == Qt.Checked; }
- indicator: Rectangle {
- implicitWidth: 16 * virtualstudio.uiScale
- implicitHeight: 16 * virtualstudio.uiScale
- x: showStartupSetup.leftPadding
- y: parent.height / 2 - height / 2
- radius: 3 * virtualstudio.uiScale
- border.color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
-
- Rectangle {
- width: 10 * virtualstudio.uiScale
- height: 10 * virtualstudio.uiScale
- x: 3 * virtualstudio.uiScale
- y: 3 * virtualstudio.uiScale
- radius: 2 * virtualstudio.uiScale
- color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
- visible: showStartupSetup.checked
- }
- }
- contentItem: Text {
- text: showStartupSetup.text
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- leftPadding: showStartupSetup.indicator.width + showStartupSetup.spacing
- color: textColour
- }
- }
-
- Text {
- anchors.verticalCenter: showStartupSetup.verticalCenter
- x: 48 * virtualstudio.uiScale
- text: "Device Setup"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- CheckBox {
- id: showStartupWarnings
- checked: virtualstudio.showWarnings
- text: qsTr("Show recommendations on startup again next time")
- x: updateChannelCombo.x; y: showStartupSetup.y + (48 * virtualstudio.uiScale)
- onClicked: { virtualstudio.showWarnings = showStartupWarnings.checkState == Qt.Checked; }
- indicator: Rectangle {
- implicitWidth: 16 * virtualstudio.uiScale
- implicitHeight: 16 * virtualstudio.uiScale
- x: showStartupWarnings.leftPadding
- y: parent.height / 2 - height / 2
- radius: 3 * virtualstudio.uiScale
- border.color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
-
- Rectangle {
- width: 10 * virtualstudio.uiScale
- height: 10 * virtualstudio.uiScale
- x: 3 * virtualstudio.uiScale
- y: 3 * virtualstudio.uiScale
- radius: 2 * virtualstudio.uiScale
- color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
- visible: showStartupWarnings.checked
- }
- }
- contentItem: Text {
- text: showStartupWarnings.text
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- leftPadding: showStartupWarnings.indicator.width + showStartupWarnings.spacing
- color: textColour
- }
- }
-
- Text {
- anchors.verticalCenter: showStartupWarnings.verticalCenter
- x: 48 * virtualstudio.uiScale
- text: "Recommendations"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
- }
-
- Rectangle {
- id: profileSettingsView
- width: 0.8 * parent.width
- height: parent.height - header.height
- x: 0.2 * window.width
- y: header.height
- color: backgroundColour
- visible: settingsGroupView == "Profile"
-
- Image {
- id: profilePicture
- width: 96; height: 96
- y: 60 * virtualstudio.uiScale
- source: virtualstudio.userMetadata.picture ? virtualstudio.userMetadata.picture : ""
- anchors.horizontalCenter: parent.horizontalCenter
- fillMode: Image.PreserveAspectCrop
- }
-
- Text {
- id: displayName
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: profilePicture.bottom
- text: virtualstudio.userMetadata.user_metadata ? ( virtualstudio.userMetadata.user_metadata.display_name ? virtualstudio.userMetadata.user_metadata.display_name : virtualstudio.userMetadata.nickname ) : virtualstudio.userMetadata.name || ""
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Text {
- id: email
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: displayName.bottom
- text: virtualstudio.userMetadata.email ? virtualstudio.userMetadata.email : ""
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- Button {
- id: editButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: editButton.down ? buttonPressedColour : (editButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: editButton.down ? buttonPressedStroke : (editButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { virtualstudio.editProfile(); }
- anchors.horizontalCenter: parent.horizontalCenter
- y: email.y + (56 * virtualstudio.uiScale)
- width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Edit Profile"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Button {
- id: logoutButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: { virtualstudio.showFirstRun = false; virtualstudio.logout(); }
- anchors.horizontalCenter: parent.horizontalCenter
- y: editButton.y + (48 * virtualstudio.uiScale)
- width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Log Out"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Button {
- id: testModeButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: {
- virtualstudio.testMode = !virtualstudio.testMode;
-
- // behave like "Cancel" and switch back to browse mode
- audio.stopAudio();
- virtualstudio.windowState = "browse";
- virtualstudio.loadSettings();
- audio.validateDevices();
- }
- anchors.horizontalCenter: parent.horizontalCenter
- y: logoutButton.y + (48 * virtualstudio.uiScale)
- width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false
- Text {
- text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
- }
-
- Rectangle {
- x: -1; y: parent.height - (36 * virtualstudio.uiScale)
- width: parent.width; height: (36 * virtualstudio.uiScale)
- border.color: "#33979797"
- color: backgroundColour
-
- Button {
- id: cancelButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: {
- audio.stopAudio();
- virtualstudio.windowState = "browse";
- virtualstudio.loadSettings();
- audio.validateDevices();
- }
- anchors.verticalCenter: parent.verticalCenter
- x: parent.width - (230 * virtualstudio.uiScale)
- width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
- Text {
- text: "Cancel"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: textColour
- }
- }
-
- Button {
- id: saveButton
- enabled: !Boolean(audio.devicesError)
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour)
- border.width: 1
- border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke)
- }
- onClicked: {
- audio.stopAudio();
- virtualstudio.windowState = "browse";
- virtualstudio.saveSettings();
- }
- anchors.verticalCenter: parent.verticalCenter
- x: parent.width - (119 * virtualstudio.uiScale)
- width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
- Text {
- text: "Save"
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
- color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour
- }
- }
-
- DeviceWarning {
- id: deviceWarning
- x: (0.2 * window.width) + 16 * virtualstudio.uiScale
- anchors.verticalCenter: parent.verticalCenter
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
- }
-
- Connections {
- target: audio
- // anything that sets currentIndex to the value of a function needs
- // to be manually updated whenever there is a change to any vars it uses
- function onBufferSizeChanged() {
- bufferCombo.currentIndex = getCurrentBufferSizeIndex();
- }
- function onAudioBackendChanged() {
- backendCombo.currentIndex = getCurrentAudioBackendIndex();
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- property int fontBig: 20
- property int fontMedium: 13
- property int fontSmall: 11
- property int fontExtraSmall: 8
-
- property int leftMargin: 48
- property int rightMargin: 16
-
- property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
- property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
- property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
- property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
- property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
- property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
- property string saveButtonBackgroundColour: "#F2F3F3"
- property string saveButtonPressedColour: "#E7E8E8"
- property string saveButtonStroke: "#EAEBEB"
- property string saveButtonPressedStroke: "#B0B5B5"
- property string saveButtonText: "#DB0A0A"
- property string checkboxStroke: "#0062cc"
- property string checkboxPressedStroke: "#007AFF"
- property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-
- Item {
- id: setupItem
- width: parent.width; height: parent.height
-
- property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
-
- Text {
- id: pageTitle
- x: 16 * virtualstudio.uiScale;
- y: 16 * virtualstudio.uiScale
- text: "Choose your audio devices"
- font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- }
-
- DeviceRefreshButton {
- id: refreshButton
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.verticalCenter: pageTitle.verticalCenter
- visible: parent.isUsingRtAudio
- enabled: audio.audioReady && !audio.scanningDevices
- }
-
- Text {
- text: "Restarting Audio"
- anchors.verticalCenter: pageTitle.verticalCenter;
- anchors.right: refreshButton.left;
- anchors.rightMargin: 16 * virtualstudio.uiScale;
- font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- visible: !audio.audioReady
- }
-
- AudioSettings {
- id: audioSettings
- width: parent.width
- anchors.top: pageTitle.bottom
- anchors.topMargin: 16 * virtualstudio.uiScale
- }
-
- Rectangle {
- id: headerBorder
- width: parent.width
- height: 1
- anchors.top: audioSettings.top
- color: strokeColor
- }
-
- Rectangle {
- id: footerBorder
- width: parent.width
- height: 1
- anchors.top: audioSettings.bottom
- color: strokeColor
- }
-
- Rectangle {
- property int footerHeight: (30 + (rightMargin * 2)) * virtualstudio.uiScale;
- x: -1; y: parent.height - footerHeight;
- width: parent.width; height: footerHeight;
- border.color: "#33979797"
- color: backgroundColour
-
- Button {
- id: backButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: backButton.down ? buttonPressedColour : buttonColour
- border.width: 1
- border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke
- }
- onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); }
- anchors.left: parent.left
- anchors.leftMargin: 16 * virtualstudio.uiScale
- anchors.bottomMargin: rightMargin * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Back"
- font.family: "Poppins"
- font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
- color: textColour
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- DeviceWarning {
- id: deviceWarning
- anchors.left: backButton.right
- anchors.leftMargin: 16 * virtualstudio.uiScale
- anchors.verticalCenter: backButton.verticalCenter
- visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
- }
-
- Button {
- id: saveButton
- background: Rectangle {
- radius: 6 * virtualstudio.uiScale
- color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
- border.width: 1
- border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke
- }
- enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady
- onClicked: {
- audio.stopAudio(true);
- virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
- virtualstudio.windowState = "connected";
- virtualstudio.saveSettings();
- virtualstudio.joinStudio();
- }
- anchors.right: parent.right
- anchors.rightMargin: rightMargin * virtualstudio.uiScale
- anchors.bottomMargin: rightMargin * virtualstudio.uiScale
- anchors.bottom: parent.bottom
- width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
- Text {
- text: "Connect to Studio"
- font.family: "Poppins"
- font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
- font.weight: Font.Bold
- color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- }
- }
-
- CheckBox {
- id: showAgainCheckbox
- checked: virtualstudio.showDeviceSetup
- visible: audio.backendAvailable
- text: qsTr("Ask again next time")
- anchors.right: saveButton.left
- anchors.rightMargin: 16 * virtualstudio.uiScale
- anchors.verticalCenter: saveButton.verticalCenter
- onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked }
- indicator: Rectangle {
- implicitWidth: 16 * virtualstudio.uiScale
- implicitHeight: 16 * virtualstudio.uiScale
- x: showAgainCheckbox.leftPadding
- y: parent.height / 2 - height / 2
- radius: 3 * virtualstudio.uiScale
- border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
-
- Rectangle {
- width: 10 * virtualstudio.uiScale
- height: 10 * virtualstudio.uiScale
- x: 3 * virtualstudio.uiScale
- y: 3 * virtualstudio.uiScale
- radius: 2 * virtualstudio.uiScale
- color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
- visible: showAgainCheckbox.checked
- }
- }
-
- contentItem: Text {
- text: showAgainCheckbox.text
- font.family: "Poppins"
- font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing
- color: textColour
- }
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import Qt5Compat.GraphicalEffects
-import VS 1.0
-
-Rectangle {
- width: 664; height: 83 * virtualstudio.uiScale
- radius: 6 * virtualstudio.uiScale
- color: backgroundColour
-
- property string serverLocation: "Germany - Berlin"
- property string flagImage: "flags/DE.svg"
- property string hostname: "app.jacktrip.com"
- property string studioName: "Test Studio"
- property string studioId: ""
- property string streamId: ""
- property string inviteKeyString: ""
- property int sampleRate: 48000
- property bool publicStudio: false
- property bool admin: 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
- property real fontSmall: 8
-
- property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
- property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
- property string inviteToolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
- property string inviteToolTipTextColour: textColour
- property string inviteCopiedBackgroundColour: "#57B147"
- property string inviteCopiedTextColour: "#FAFBFB"
- property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-
- property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB"
- property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3"
- property string baseButtonPressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB"
- property string baseButtonStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494"
-
- property string joinAvailableColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
- property string joinAvailableHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
- property string joinAvailablePressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
- property string joinAvailableStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
-
- property string joinUnavailableColour: baseButtonColour
- property string joinUnavailableHoverColour: baseButtonHoverColour
- property string joinUnavailablePressedColour: baseButtonPressedColour
- property string joinUnavailableStroke: baseButtonStroke
-
- property string startColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
- property string startHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
- property string startPressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
- property string startStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
-
- property string manageColour: baseButtonColour
- property string manageHoverColour: baseButtonHoverColour
- property string managePressedColour: baseButtonPressedColour
- property string manageStroke: baseButtonStroke
-
- property string leaveColour: virtualstudio.darkMode ? "#FCB6B6" : "#FCB6B6"
- property string leaveHoverColour: virtualstudio.darkMode ? "#D49696" : "#E3A4A4"
- property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD"
- property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E"
-
- property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-
- border.width: 1
- border.color: studioStroke
-
- Clipboard {
- id: clipboard
- }
-
- Rectangle {
- id: shadow
- anchors.fill: parent
- color: "transparent"
- radius: 6
- }
-
- Rectangle {
- width: 12 * virtualstudio.uiScale; height: parent.height
- radius: width / 2
- color: available ? "#0C1424" : "#B3B3B3"
- }
-
- Image {
- id: wedge
- source: available ? "wedge.svg" : "wedge_inactive.svg"
- x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale
- sourceSize: Qt.size(wedge.width,wedge.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
-
- Image {
- id: studioLogo
- source: "logo.svg"
- x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
- sourceSize: Qt.size(studioLogo.width,studioLogo.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
-
- Rectangle {
- x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale
- width: 32 * virtualstudio.uiScale; height: width
- radius: width / 2
- color: available ? "#0C1424" : "#B3B3B3"
- }
-
- Image {
- id: flag
- source: flagImage
- x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale
- width: 40 * virtualstudio.uiScale; height: width / 4 * 3
- fillMode: Image.PreserveAspectCrop
- layer.enabled: true
- layer.effect: OpacityMask {
- maskSource: mask
- }
-
- AppIcon {
- id: defaultFlag
- anchors.fill: parent
- width: 32 * virtualstudio.uiScale
- height: 32 * virtualstudio.uiScale
- icon.source: "language.svg"
- color: "white"
- visible: flag.status != Image.Ready
- }
- }
-
- Rectangle {
- id: mask
- x: 0 ; y: 0 ; width: flag.width; height: flag.height
- visible: false
- color: "#00000000"
- Rectangle {
- x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale
- width:24 * virtualstudio.uiScale; height: width
- radius: width / 2
- }
- }
-
- Text {
- x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale;
- width: (admin || connected) ? 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
- }
-
- Rectangle {
- id: publicRect
- x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale
- width: 14 * virtualstudio.uiScale; height: width
- radius: 2 * virtualstudio.uiScale
- color: publicStudio ? "#0095FF" : "#FF9800"
- Image {
- id: pubPriv
- source: publicStudio ? "public.svg" : "private.svg"
- x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width
- sourceSize: Qt.size(pubPriv.width,pubPriv.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
- }
-
- Text {
- anchors.verticalCenter: publicRect.verticalCenter
- x: (leftMargin + 22) * virtualstudio.uiScale
- width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale)
- text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
- elide: Text.ElideRight
- color: textColour
- }
-
- Button {
- id: joinButton
- x: (admin || connected) ? 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
- color: available ? (joinButton.down ? joinAvailablePressedColour : (joinButton.hovered ? joinAvailableHoverColour : joinAvailableColour))
- : (joinButton.down ? joinUnavailablePressedColour : (joinButton.hovered ? joinUnavailableHoverColour : joinUnavailableColour))
- border.width: joinButton.down ? 1 : 0
- border.color: available ? joinAvailableStroke : joinUnavailableStroke
- }
- visible: !connected
- onClicked: {
- virtualstudio.studioToJoin = studioId;
- virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
- virtualstudio.joinStudio();
- }
- Image {
- id: join
- width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- source: "join.svg"
- sourceSize: Qt.size(join.width,join.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
- }
-
- Button {
- id: leaveButton
- x: (admin || connected) ? 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
- color: leaveButton.down ? leavePressedColour : (leaveButton.hovered ? leaveHoverColour : leaveColour)
- border.width: leaveButton.down ? 1 : 0
- border.color: leaveStroke
- }
- visible: connected
- onClicked: {
- virtualstudio.disconnect();
- }
- Image {
- id: leave
- width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- source: "leave.svg"
- sourceSize: Qt.size(leave.width,leave.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
- }
-
- Text {
- anchors.horizontalCenter: joinButton.horizontalCenter
- y: 56 * virtualstudio.uiScale
- text: connected ? "Leave" : "Join"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
- visible: true
- color: textColour
- }
-
- Button {
- id: inviteButton
- x: (admin || connected) ? 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: 3000; running: false; repeat: false
- onTriggered: inviteCopied = false;
- }
- onClicked: {
- inviteCopied = true;
- if (virtualstudio.testMode) {
- hostname = "test.jacktrip.com";
- }
- if (!inviteKeyString) {
- clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=true"));
- } else {
- clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=" + inviteKeyString));
- }
- copiedResetTimer.restart()
- }
- visible: true
- Image {
- id: shareImg
- width: 24 * virtualstudio.uiScale; height: width
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- source: "share.svg"
- sourceSize: Qt.size(shareImg.width,shareImg.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
- ToolTip {
- parent: inviteButton
- visible: !inviteCopied && inviteButton.hovered
- bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
- rightPadding: rightToolTipMargin * virtualstudio.uiScale
- delay: 100
- contentItem: Rectangle {
- color: inviteToolTipBackgroundColour
- radius: 3
- anchors.fill: parent
- anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
- anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
- layer.enabled: true
- border.width: 1
- border.color: tooltipStroke
-
- Text {
- anchors.centerIn: parent
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
- text: qsTr("Copy invite link for Studio")
- color: inviteToolTipTextColour
- }
- }
- background: Rectangle {
- color: "transparent"
- }
- }
- ToolTip {
- parent: inviteButton
- visible: inviteCopied
- bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
- rightPadding: rightToolTipMargin * virtualstudio.uiScale
- delay: 100
- contentItem: Rectangle {
- color: inviteCopiedBackgroundColour
- radius: 3
- anchors.fill: parent
- anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
- anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
- layer.enabled: true
- border.width: 1
- border.color: tooltipStroke
-
- Text {
- anchors.centerIn: parent
- font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
- text: qsTr("📋 Copied invitation link to Clipboard")
- color: inviteCopiedTextColour
- }
- }
- 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
- x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale
- width: 40 * virtualstudio.uiScale; height: width
- background: Rectangle {
- radius: width / 2
- color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour)
- border.width: manageButton.down ? 1 : 0
- border.color: manageStroke
- }
- onClicked: {
- var url = "";
- if (streamId === "") {
- if (virtualstudio.testMode) {
- url = "https://test.jacktrip.com/studios/" + studioId;
- } else {
- url = "https://app.jacktrip.com/studios/" + studioId;
- }
- } else {
- if (virtualstudio.testMode) {
- url = "https://next-test.jacktrip.com/@" + streamId + "/dashboard";
- } else {
- url = "https://www.jacktrip.com/@" + streamId + "/dashboard";
- }
- }
- virtualstudio.openLink(qsTr(url));
- }
- visible: admin || connected
- Image {
- id: manageImg
- width: 20 * virtualstudio.uiScale; height: width
- anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- source: "manage.svg"
- sourceSize: Qt.size(manageImg.width,manageImg.height)
- fillMode: Image.PreserveAspectFit
- smooth: true
- }
- }
-
- Text {
- anchors.horizontalCenter: manageButton.horizontalCenter
- y: 56 * virtualstudio.uiScale
- text: "Manage"
- font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
- visible: admin || connected
- color: textColour
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-
-Item {
- width: parent.width
- height: parent.height
-
- required property string labelText
- required property string tooltipText
- required property bool sliderEnabled
- property bool showLabel: true
- property int fontSize: 8
-
- property string iconColor: virtualstudio.darkMode ? "#ACAFAF" : "gray"
- property string sliderPressedColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC"
-
- Item {
- anchors.fill: parent
- anchors.verticalCenter: parent.verticalCenter
-
- Text {
- id: label
- anchors.left: parent.left
- anchors.verticalCenter: parent.verticalCenter
- width: 40 * virtualstudio.uiScale
- horizontalAlignment: Text.AlignRight
- text: labelText
- font {family: "Poppins"; weight: Font.Medium; pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale }
- color: textColour
- visible: showLabel
- }
-
- InfoTooltip {
- id: tooltip
- content: tooltipText
- size: 16
- anchors.left: label.right
- anchors.leftMargin: 2 * virtualstudio.uiScale
- anchors.verticalCenter: label.verticalCenter
- visible: showLabel
- }
-
- AppIcon {
- id: quieterIcon
- anchors.left: showLabel ? tooltip.right : parent.left
- anchors.leftMargin: showLabel ? 8 * virtualstudio.uiScale : 0
- anchors.verticalCenter: label.verticalCenter
- width: 16
- height: 16
- icon.source: "quiet.svg"
- color: iconColor
- }
-
- AppIcon {
- id: louderIcon
- anchors.right: parent.right
- anchors.verticalCenter: label.verticalCenter
- width: 18
- height: 18
- icon.source: "loud.svg"
- color: iconColor
- }
-
- Slider {
- id: slider
- value: labelText == "Monitor" ? audio.monitorVolume : (labelText == "Studio" ? audio.outputVolume : audio.inputVolume )
- onMoved: {
- if (labelText == "Monitor") {
- audio.monitorVolume = value;
- } else if (labelText == "Studio") {
- audio.outputVolume = value;
- } else {
- audio.inputVolume = value;
- }
- }
- enabled: sliderEnabled
- from: 0.0
- to: 1.0
- stepSize: 0.01
- padding: 0
- anchors.left: quieterIcon.right
- anchors.leftMargin: 4 * virtualstudio.uiScale
- anchors.right: louderIcon.left
- anchors.rightMargin: 4 * virtualstudio.uiScale
- anchors.verticalCenter: label.verticalCenter
-
- background: Rectangle {
- x: slider.leftPadding
- y: slider.topPadding + slider.availableHeight / 2 - height / 2
- implicitWidth: parent.width
- implicitHeight: 8 * virtualstudio.uiScale
- width: slider.availableWidth
- height: implicitHeight
- radius: 4
- color: "gray"
-
- Rectangle {
- width: slider.visualPosition * parent.width
- height: parent.height
- radius: 4
- gradient: Gradient {
- orientation: Gradient.Horizontal
- GradientStop { position: 0.0; color: sliderEnabled ? "#67C6F3" : "light gray" }
- GradientStop { position: 1.0; color: sliderEnabled ? "#00897b" : "light gray" }
- }
- }
- }
-
- handle: Rectangle {
- x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
- y: slider.topPadding + slider.availableHeight / 2 - height / 2
- implicitWidth: 18 * virtualstudio.uiScale
- implicitHeight: 18 * virtualstudio.uiScale
- radius: implicitWidth / 2
- color: slider.pressed ? sliderPressedColour : "white"
- border.width: 3
- border.color: sliderEnabled ? "#00897b" : "light gray"
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Loader {
- anchors.fill: parent
- source: "WebEngine.qml"
-
- // TODO: Add support for QtWebView
- // source: useWebEngine ? "WebEngine.qml" : "WebView.qml"
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtWebEngine
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- function contentScriptFactory (port) {
- return `
- // add script tag for qwebchannel
- document.head.addEventListener("initqwebchannel", () => {
-
- var script = document.createElement("script");
- script.onload = function () {
- var url = "ws://localhost:${port}";
- var socket = new WebSocket(url);
-
- socket.onclose = function() {
- console.error("[QT] web channel closed");
- };
- socket.onerror = function(event) {
- console.error("[QT] web channel error: " + event.type);
- };
- socket.onopen = function() {
- new QWebChannel(socket, function(channel) {
- console.log("[QT] Socket opened");
-
- // make core object accessible globally
- window.virtualstudio = channel.objects.virtualstudio;
- window.auth = channel.objects.auth;
- window.clipboard = channel.objects.clipboard;
-
- const event = new CustomEvent("qwebchannelinitialized");
- document.head.dispatchEvent(event);
- console.log("[QT] Dispatched qwebchannelinitialized event");
- console.log("[QT] Connected to WebChannel, ready to send/receive messages!");
- });
- }
- }
- script.setAttribute("src", "qrc:///qtwebchannel/qwebchannel.js");
- script.setAttribute("type", "text/javascript");
- document.head.appendChild(script);
- console.log("[QT] Added qwebchannel initialization script to DOM.");
- });
- console.log("[QT] Added initqwebchannel event listener");
- `
- }
-
- Rectangle {
- id: web
- anchors.fill: parent
- color: backgroundColour
-
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
- property string studioId: virtualstudio.currentStudio.id
-
- WebEngineView {
- id: webEngineView
- anchors.fill: parent
- settings.javascriptCanAccessClipboard: true
- settings.javascriptCanPaste: true
- settings.screenCaptureEnabled: true
- profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
- url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
-
- // useful for debugging
- // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) {
- // console.log(level, message, lineNumber, sourceID);
- // }
-
- // useful for debugging
- // onLoadingChanged: function(loadRequest) {
- // console.log("onLoadingChanged", loadRequest.errorCode, loadRequest.errorDomain, loadRequest.errorString, loadRequest.status, loadRequest.url);
- // }
-
- 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);
- }
-
- onNavigationRequested: function(request) {
- webEngineView.userScripts.collection = [
- {
- name: "script",
- sourceCode: contentScriptFactory(virtualstudio.webChannelPort),
- injectionPoint: WebEngineScript.DocumentReady,
- worldId: WebEngineScript.MainWorld
- }
- ]
- }
- }
- }
-}
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- Item {
- id: webNull
- anchors.fill: parent
- }
-}
+++ /dev/null
-/****************************************************************************
-**
-** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
-*author Milian Wolff <milian.wolff@kdab.com>
-** Contact: http://www.qt.io/licensing/
-**
-** This file is part of the QtWebChannel module of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:LGPL21$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see http://www.qt.io/terms-conditions. For further
-** information use the contact form at http://www.qt.io/contact-us.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 2.1 or version 3 as published by the Free
-** Software Foundation and appearing in the file LICENSE.LGPLv21 and
-** LICENSE.LGPLv3 included in the packaging of this file. Please review the
-** following information to ensure the GNU Lesser General Public License
-** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
-** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
-**
-** As a special exception, The Qt Company gives you certain additional
-** rights. These rights are described in The Qt Company LGPL Exception
-** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
-
-#include "WebSocketTransport.h"
-
-#include <QDebug>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QtWebSockets/QWebSocket>
-
-/*!
- \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally.
- The transport delegates all messages received over the QWebSocket over its
- textMessageReceived signal. Analogously, all calls to sendTextMessage will
- be send over the QWebSocket to the remote client.
-*/
-
-QT_BEGIN_NAMESPACE
-
-/*!
- Construct the transport object and wrap the given socket.
- The socket is also set as the parent of the transport object.
-*/
-WebSocketTransport::WebSocketTransport(QWebSocket* socket)
- : QWebChannelAbstractTransport(socket), m_socket(socket)
-{
- connect(socket, &QWebSocket::textMessageReceived, this,
- &WebSocketTransport::textMessageReceived);
-}
-
-/*!
- Destroys the WebSocketTransport.
-*/
-WebSocketTransport::~WebSocketTransport() {}
-
-/*!
- Serialize the JSON message and send it as a text message via the WebSocket to the
- client.
-*/
-void WebSocketTransport::sendMessage(const QJsonObject& message)
-{
- QJsonDocument doc(message);
- m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
-}
-
-/*!
- Deserialize the stringified JSON messageData and emit messageReceived.
-*/
-void WebSocketTransport::textMessageReceived(const QString& messageData)
-{
- QJsonParseError error;
- QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error);
- if (error.error) {
- qWarning() << "Failed to parse text message as JSON object:" << messageData
- << "Error is:" << error.errorString();
- return;
- } else if (!message.isObject()) {
- qWarning() << "Received JSON message that is not an object: " << messageData;
- return;
- }
- emit messageReceived(message.object(), this);
-}
-
-QT_END_NAMESPACE
\ No newline at end of file
+++ /dev/null
-/****************************************************************************
-**
-** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
-*author Milian Wolff <milian.wolff@kdab.com>
-** Contact: http://www.qt.io/licensing/
-**
-** This file is part of the QtWebChannel module of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:LGPL21$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see http://www.qt.io/terms-conditions. For further
-** information use the contact form at http://www.qt.io/contact-us.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 2.1 or version 3 as published by the Free
-** Software Foundation and appearing in the file LICENSE.LGPLv21 and
-** LICENSE.LGPLv3 included in the packaging of this file. Please review the
-** following information to ensure the GNU Lesser General Public License
-** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
-** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
-**
-** As a special exception, The Qt Company gives you certain additional
-** rights. These rights are described in The Qt Company LGPL Exception
-** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
-
-#ifndef WEBSOCKETTRANSPORT_H
-#define WEBSOCKETTRANSPORT_H
-
-#include <QtWebChannel/QWebChannelAbstractTransport>
-
-QT_BEGIN_NAMESPACE
-
-class QWebSocket;
-class WebSocketTransport : public QWebChannelAbstractTransport
-{
- Q_OBJECT
- public:
- explicit WebSocketTransport(QWebSocket* socket);
- virtual ~WebSocketTransport();
-
- void sendMessage(const QJsonObject& message) Q_DECL_OVERRIDE;
-
- private Q_SLOTS:
- void textMessageReceived(const QString& message);
-
- private:
- QWebSocket* m_socket;
-};
-
-QT_END_NAMESPACE
-
-#endif // WEBSOCKETTRANSPORT_H
\ No newline at end of file
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-import QtWebView
-
-Item {
- width: parent.width; height: parent.height
- clip: true
-
- Item {
- id: web
- anchors.fill: parent
-
- property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
- property string studioId: virtualstudio.currentStudio.id
-
- WebView {
- id: webEngineView
- anchors.fill: parent
- httpUserAgent: `JackTrip/${virtualstudio.versionString}`
- url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
- }
- }
-}
m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString));
#ifdef __APPLE__
- m_ui->aboutImage->setPixmap(QPixmap(":/qjacktrip/about@2x.png"));
+ m_ui->aboutImage->setPixmap(QPixmap(":/images/icon_256.png"));
#endif
aboutText.setHtml(m_ui->aboutLabel->text());
<string/>
</property>
<property name="pixmap">
- <pixmap resource="qjacktrip.qrc">:/qjacktrip/about.png</pixmap>
+ <pixmap resource="images.qrc">:/images/icon_128.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="240" viewBox="0 96 960 960" width="240"><path d="M421 676.692 320.077 574q-7.154-5.385-16.615-5.769-9.462-.385-15.847 6-7.154 7.154-7.154 16.615 0 9.462 7.154 15.616l109.923 110.154q9.049 11 23.371 11t24.553-11l227.153-226.385q5.616-6.385 6-15.846.385-9.462-6-16.847-7.384-6.153-16.961-6.038-9.577.115-15.731 6.269L421 676.692ZM480.134 952q-78.082 0-146.274-29.859-68.193-29.86-119.141-80.762-50.947-50.902-80.833-119.033Q104 654.215 104 576.134q0-77.569 29.918-146.371 29.919-68.803 80.922-119.917 51.003-51.114 119.032-80.48Q401.901 200 479.866 200q77.559 0 146.353 29.339 68.794 29.34 119.922 80.422 51.127 51.082 80.493 119.841Q856 498.361 856 575.95q0 78.358-29.339 146.21-29.34 67.853-80.408 118.902-51.069 51.048-119.81 80.993Q557.702 952 480.134 952ZM480 908.231q137.897 0 235.064-97.282Q812.231 713.666 812.231 576q0-137.897-97.167-235.064T480 243.769q-137.666 0-234.949 97.167Q147.769 438.103 147.769 576q0 137.666 97.282 234.949Q342.334 908.231 480 908.231ZM480 576Z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10 13.5C9.07177 13.5 8.18153 13.1313 7.52515 12.4749C6.86877 11.8185 6.50002 10.9283 6.50002 10C6.50002 9.07174 6.86877 8.1815 7.52515 7.52513C8.18153 6.86875 9.07177 6.5 10 6.5C10.9283 6.5 11.8185 6.86875 12.4749 7.52513C13.1313 8.1815 13.5 9.07174 13.5 10C13.5 10.9283 13.1313 11.8185 12.4749 12.4749C11.8185 13.1313 10.9283 13.5 10 13.5V13.5ZM17.43 10.97C17.47 10.65 17.5 10.33 17.5 10C17.5 9.67 17.47 9.34 17.43 9L19.54 7.37C19.73 7.22 19.78 6.95 19.66 6.73L17.66 3.27C17.54 3.05 17.27 2.96 17.05 3.05L14.56 4.05C14.04 3.66 13.5 3.32 12.87 3.07L12.5 0.42C12.46 0.18 12.25 0 12 0H8.00002C7.75002 0 7.54002 0.18 7.50002 0.42L7.13002 3.07C6.50002 3.32 5.96002 3.66 5.44002 4.05L2.95002 3.05C2.73002 2.96 2.46002 3.05 2.34002 3.27L0.340022 6.73C0.210022 6.95 0.270023 7.22 0.460023 7.37L2.57002 9C2.53002 9.34 2.50002 9.67 2.50002 10C2.50002 10.33 2.53002 10.65 2.57002 10.97L0.460023 12.63C0.270023 12.78 0.210022 13.05 0.340022 13.27L2.34002 16.73C2.46002 16.95 2.73002 17.03 2.95002 16.95L5.44002 15.94C5.96002 16.34 6.50002 16.68 7.13002 16.93L7.50002 19.58C7.54002 19.82 7.75002 20 8.00002 20H12C12.25 20 12.46 19.82 12.5 19.58L12.87 16.93C13.5 16.67 14.04 16.34 14.56 15.94L17.05 16.95C17.27 17.03 17.54 16.95 17.66 16.73L19.66 13.27C19.78 13.05 19.73 12.78 19.54 12.63L17.43 10.97Z" fill="#494646"/>
-</svg>
+++ /dev/null
-<svg width="179" height="128" viewBox="0 0 179 128" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <image id="b" x="0" y="0" width="179" height="128" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALMAAACACAYAAABA+ljwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABLPSURBVHgB7V0HsCxFFT0GjIiICeNrMGMAMcf/AMtQRkRFUeSpKIgKmFBA2fkqiGJE+CgGnmJAxZzje5SlRZkFFDHNA8UESFCy8O3j7NTv7Z2e7p7pnrA7p+rU/2+mZ6Z79uzd2923bwMDBvhjNOaAAb3GwZIbxzwcAwb0FK/HJiF3TtDXQXs4QHJXyftLXh/zh39L/kjydZK/R/dBIb/VcO4IyUMxp/g4pr/h88y90W3wC1dW/wslb4U5BH+W+iCwJnmt5H3RTYxgF/KDMac4B/0QWNM8Bt2DzSJfLPkg7ZpFzAkE+iGsNvhHdAsHoby+F0nuoF1z2PjcXIxyCPRDWG0wRXcwQnldz5Pc0XLNrmgYszyK8ATJq1Afu0gegvnBayWTkvO0yDtJnqEcG2nXnCj5Rcw4BJqxcuchHB4JNFLnFO1jhPI6/kPy3pZrPiF5XcwBBJoRxgrCQQCN1DlFu3gNyuvHUQvdRx5pZT4teT2tDMU9kz60QDPC+BzC4UaSlwPR65yiPbwadiHfU7tmpJU5GZNu62bI3I38/MwJWqAZMR+FsEiB6HVuS8yHobxe58Jukekfb6aVOVYrk2DGINCMmEPPqP0AiF7nFM3jVbBbZF3IiVbma5I3VM7zl+yTaEHIfR/N+K7k9wqOfx9h8XZkH5oOxpdsjX7ijZJvKjn/F8mdMRk3kmAy9PPbkrtJXqkce7/kc5S/12NSzAuST5N8L3oOgbDW7GC0i1X00zK/0lKXSzBtkddrZWgwbqyV+QrKLfJtJH8zPtd7H1ogrJj3QLv4GvonZpuQc46Ua7ZHFj+SnztF8ibK+c2RdQDV69+sPZe/YGcq56+QvBd6DIGwYt4F7WIZ/RLzG+BXp5Fy7eOQCfqHkjdVjjOM+GMot8i3kzxLK7OEnkMgrJgX0S6W0R8xH4hq9Rop93iq5M2UvynkE7XyuvtwB8nfaWV218oI9BACYcV8f7SLo9APMR+KenUbFdyTs3y6kBOtjEDWgVTLvFAr8xhkoyZFz/BC10YzvoFsytQVMQXgghX4BaUvonkrxBGXt6Aeni/5bmQdwxxflnyi8rc+anEnZKNNd9Huc6Ly9yMkv45sjJrXXobwcwTRIGD+9m/A7EPAPAET44u5P+r/YvwRxV/Ap2NTpzDRzt1pfF1+D5bTLfIzJK/GZPsFegQB80ubF3AEpgkxH4Iwrs+dlXvyl/xZyt/sFCbac1n+T9p9XqKVoUW/CD0WMiEwiPmZiC/mVyCMRVaFzACiVZh9aGIB00LeRyvD8NFrUC5k3v9YdBwCg5hjizmUkIVyTwr5JK2MLugFTLtQ+2llOPN3MexCzs93WtACg5hjivn1iGORTZNDqqD3RJY+IT+3v1a3xyLrQJYJWZ9l5Cr+NtNhlEJgEHMsMb8c4S3yFrDPciZK+bxTeKBWt70wOYNYJOSRdt8T0HEIDGKOIWbb4lMX8vm6Rf6047Uj5bptMd3eS+EnZAb0qyGltM67oWMQGMQcWswvQxiLbOrsuXJUULenFLRRaGV0IX9eO69Ol78LHYLAIOYiMXMG7GXwRyghC+WeFPJXK95rpNxnAf6uxbJ2nkI+QSuzMzoCAfOLEJgPcPWzbq12hD9sa/ZcyGfrFvmkmvccKfd7ATJBuwiZLo0aiUchf1grUxZ/3TgEyl/EPphtfAGT7f2m5O3hj/1QX8hFnb2qFlnneuW+u8Au5I9ictSiyCLrIaVbomUIlL+ETvlEEZBiU1uPKCm3Rck521KnqhbZtbNXxUKrOFIrRx95c+V8kZAT7R75LKMpK2kjEBjEzHa+tKQMg2+4iPTAgnMvRRiLXLezV1XQI+08A5F0i6zHRushpaz7acr5d6MlCAxiPqHkvB6qqeY83hFhhCyUe4Z0LVwEzWyh+cQJF72qyWKKLLJuebfC9HQ52UqkncB8i3kFWS9fB/0//twWvZMXKeVs+S3KmGLaIn+qxv2qCpq/PHRpVFeqSMhHYhJFAUymZzQCgfkW86MLjgnJP6D8vai57qoIWnctKKTQPrKPoFVQyB/RyurpfXXXoojLiAj2aLfRjgmEEzPHG/ceczvl+I7K8Z2U49soxx+jHN9SOb6n9oy9Faq9592U49ugHmwfUk411sFH0CnCD79V5QFa24ss8nqtjM0ik8chUgwHpyGXxw+JKeZl5Tq1s5Qox09Qjj9VOb5iqNPF2jPUuqnpqX6qHH8+quNg+InBV9DsSDbV2bPxdEwP0a2T/I9S5p3aeRchfygvHDpbI60Xk7LshQE2CPjnjmDilNzl4Af/Gkt55qlQF6Byenkdmsea5JPH/xL5rxxTFlArFOV7kH1Bc7Czt4ryXz4K+cWIAA7+/wKT3xofy8xl8D4D4cvot2U+A9WtnI+FVttK43VKjedWYSp5a6UOd5T8FSZ9aH0GtFXXgiL8ZcEDfcTsi2X0V8z6lHZsQasLTx8e4Nmu5BdWKM/mr4T67kaYRqudPf6UnWV46CDmaQhMhkQ2IegUky7luwI9f6PlmUJ5JvNnFBm8kVKmlkWu6zPfAFm8wd0xwBWfwWRATR3Qh84FTR/aFHknkKWuzcGAnfMRD79GNpK0Nv6bv9wM8t++oGyCTNAU8irKfWQGHuWzoFOoK+bjkf1sDXADf0EehLBQO4VM1/BaQ7mtlP9zZXSCOFiTfBImhfxjFAs5R4Ksv2UTcmlnr46Y2RPfCwNcITA9hhoK/CxyC/0OTAuaEySqK7YA+0hIFawhm7JeG//NHHOrkndzuHarknMfQCbkUne0qpgPwXztwBQCnKjYAvGguhyqoJlm9tnI4ooJCnkV4ePHc9ci3xyJFpkZqrZHPdAi7wuHflWV9FyceZuLTQsDgmv0HoL4yBN4H41M0H9GFkiUI5aQ1zDtWqwijJCdx5F9LbNAtvzbBexgXIQBAs0mRaeg81RYdC8uHf8/pkVWXYs8aCqUkK0WOYePZabwv4PMD3IB17pdiAFMwt30qgj9M4ptkVXXgvmbt0M9eAuZ8BEz98C4q2NZJiNZxQD6rQ9As+BMquoGxhQyLXIuZGZDpbGrK2R29ozDb2VwdTPo7yWOZb8k+Tb4Q2C2IND8sp4QQub2DFzSdUFJmaLOHjfr2QH14NzZK4KLmDlkchLcsIYs11kVrGC2BM2ZquuhOXDYr66Q2cd5GLIVLg9EttBUxxomO3scoWG8R93E716dvargGivXKcynW+4lLNezs/IPR15quM50/ELtWeq9XI6frxy/YnxsDcVi2Qdxp4p1Hqo9fwH+G3Hy/awraAtdiXyKmfdUg4Zc0ne5kEKOEjSk4l4eFTre4X4C9RrdNe5uaOPFDdYhlJBtLsISptcP/qRCfXUuo6HkiGc5Vogvz2Vzxy2B2o3vCt9naKNAM9sTFwm5yrP/hmkh0/d+JczIp6jr1p8bYDYi5CW4V2rJ/bb4LFD7JbTNFOXJWxYQX9AHBXimbTthuhd6rDEt8qk16p3zQ2gI3P/Y9cWk8MPWaM5yxeIDYYeI2M5Ee9YCqgn5ntp9EkPZj4zbw3jkUEJuxCIT+8K9Ykvwh0DWU97YQybwa2ca+PmxOnuLlmvOwaatguswmmthuukaivM76OASdteJlCJwV6K7oF/4keRV2rERMoEcXVBeINywY6hxZI4Rn1pwLkHc/BPMVrSETNSNYAlxrfKsQUj+F9n7OLCkTIqwFrnKfYs6exyFUlesJDXr2bhFLoPryzkXAwh9K13TKIBAdUGX5VNzvW9RZ2+Dcj6moBvr7Kl4CtwrOA+bUNrApUhF7+YwQ3mBaoLmuHXZChXbfYs6exsKyqkx6kmFepqE3LhFJnxSNtWdh+87BCYTmOjct+S6FP6ioCCrCJrXPVQre2zJc0Ja6FZcC0LAvZKnY8ALYH9PpqVJAtUtdFmWfd5XnYa/HNNC3uDwnBCCXkaLWIJ7Rd+JAQSD4G3v6iDDtQJxXQ6bjxxT0I3EWpRhFe6VHVZkb4KLoF9nuFYgnoUuGrXwfY4q6EMdr2mls6dia7g3kGOsrX7rOgjmULa9t5HhWoE4FlqFj0XW6dMpbN0iE3vAvXErmF/QTxaGc7RitndnWgsoJP8F988gJ6+xCfrYCvcts9CJoQxXiHTCyC3DvWHvwXxCIBNPCrOgmX/Yx9LlYLD7tXD/DHQLbXI5jql4T19Bfxgdgs+ce+e2fW0IX8emd5DCLGgXl0OdzXsOslGHOkIrcjlCCrlM0AxC6ozbuQC/Bi1i/lC0y1OKcgt9DcrfI12O50lejTBC46/GTuPnV+nsufKFSjt3Rcf6T9wSwacxt8R8QUiejeJ3kcIs6CXEE5SJtNAnN/CczmazSuDXkHkDP7iy95Ging/dV+6PDsInrvg8zB+YfnYZ1QXtMsoxCDoQGJ/rWvkU8wsmILS9G1NKVpdOYV/ZKZeDKwgGMbvBFohVJugmfNk2yFwbrW/KniMPLB/EvAmmiEC6HDYLzc7ittp1S+iPOH35cnQIPhU/G7MPgaydZXsS2iw0r8+XgzE38lXohzB9uR4dg0/lL8XsQ+0QH2co49IppPvmEoRUhVWmvkPzTDSbfswJvo3YDLML7hylt/e4kvI2lyMGEzSbaKaI56CjeQF9G3IPzCa4UtwkkLI1eE1uqJ4ozxVoR9B85iPQUfg2xpYcsa+wxTKUuRxfRbNCziHQrKCZlb8zIxdF8G3QqzGb4F6GNmGUdQpjC/oNhucKxBc0778zegDfhn0MswuBLIVCWftNFvrJiCuoMkEza30a6ZlcIhdzl6ygYGIQn8b9BbMNAbswdEEz+q1qPLIvRyX1Pj3gc1IU52vuNH4P/4beEbMBUxpeuhw2C527HFz93GQ+ZjK2y8EFGJ32jU2g2+Db2E7N+lSEQPZF3q3kvE3QDNiPYZG59ZgtHjqGoPk+Hoke4yj4N3oF/Uf+JWZwvEnQLp3C0EzGz+bMYZOC5i/NzdFz7An/F34JevozNIaewIWC3t1QVsBuoUMLOQeXVMUWNP3sRcwImJK2youPsZF4ExAwRwqWuRwuwqhD08SMi4UeGa4VKO8U0je+CWYI3D7tn/B/+T9BP3EizG1iQNBzDdfR5fAd+fFhKnk3w7NDW2j+vRNmFFUH/BfRP5gyd+Y0+dDc5+4ixBMzyWFPUVztYILmBvE3wwyDSbKrvPwV9BP80G2CVqftmZfiAsQVck7656b4l7qdwnWYAzDIpurLX0Q/YbPQdDko6PuhOSHnTDGdTzmHi4U2ZU6aG5yCai9+Ff2FzUKTMSZEEthXxf8d2aaiRWAGpKqdwrkAG1/1w9kD/UWddlcVco7EUpa5lu9tqDffeZmgy+KwZx4cN7Z9203kyoc+J4cZoXkh50gs1zC9g0AxigS9hhkeqfDBt1H9gzoZ/YaLy1GHv4V5oimBXdD3NVyrdgqPQb8ns4JiEfU+sL3RbzAJYExB/xLmSYq3Wa5dQ9YZLQLDT3sRb9w0fJLC6GRnaXv0Exx+44aPGyNzFdUtNEdVtsUAZ/hsn1bEFB1d7FiC+yDbA2RjQ6SFNgW8J5ZrXRKMD1BA/66uoLdGP/AAxFm6b7vnzyRvYahTYrm2c/kquownov6HyQTmAt3GXoiToCVBtpTJtvLj5zCPAiUF5TndPYxUVMB3Uf9DTdFdQbvunlRFyDlcBH2a5FaGOiZKufdhGKmoDIEwHaJU8iHoDuircufQ2ELO4Spok1AZN7MOA2oj5NjrCO3j0Ygbl3y84bkU9BmWa38teRsMiIpTEe7D5gcq0Dxo9d6OeCJW+cGSOpxmuZb9jL50nHsJgfDDVkdI3h7xcSNk2/3+HQhafxs/YKjPrSV/UXIdF8cegAFRwWD10B/4GrIhpjsjPGgFKYqYK0NsNC2DoqCLfOgU87mTVys4EnE+dCY7/7jk41Efi8iixZqYADka9jjnowz11AXNdztTa/H6gNgZLzkd/gVkrsEisoTdRfvMXR+Z+/MoZL19Bjk1OYN3+LgeDP4521L2SBSDgv7SuA0DWgCHtUJ2CF14JTKXJFVYd0fTOnyr9k4oaJuFPhwDOgkK+ldoT0xt0jSN7GKhy3I8D2gRnIJlqoE+CDAUE5TDxUIPU9EdBUcM6gTz94UMfH8J3MBt04oE/Tt0ONv8gE2gP9gXYfqSKzweCj+oLge/CBzR6E1+4wHAG9EvkbpwVfK2qAYK+qcYYip6C2b8SdEvwRaRs3AjDJh7MJnMBvRLvCoH33bAFB6HfllpjlsnKJ6gGTDg/+DOVD4bzTdNuhSfQf/WLA5oCQJZHEPT+37Y+C3Jh2HAgAq4HbJIthTtCZgZ/zkTtwMGDAgEJixZRvx8xySj8lYk98Owhm5AZDwWWVoprlQOJWAu8+cGPEsYBDw36FrvncJ7MLKkLAz/3G58LOfmyOp8GTKrzhm2v0r+SfJMZNF1TM17LgbMHf4H4Vg2S7fqhVUAAAAASUVORK5CYII="/>
-</svg>
+++ /dev/null
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 8.29504L6 14.295L7.41 15.705L12 11.125L16.59 15.705L18 14.295L12 8.29504Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M16.59 8.29504L12 12.875L7.41 8.29504L6 9.70504L12 15.705L18 9.70504L16.59 8.29504Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M184-692q-14-15-19-34t-5-39q0-47.917 33.25-81.458Q226.5-880 274-880t80.75 33.542Q388-812.917 388-765q0 20-5 39t-19 34H184ZM396-80q-63.938 0-109.469-45Q241-170 241-235h-20q-6 0-10.125-3.889T206-249l-36-369q-2-13.5 7.25-23.25T200-651h148q13.5 0 22.75 9.75T378-618l-36 369q-.75 6.222-4.875 10.111Q333-235 327-235h-26q0 39 27.867 67 27.868 28 67 28Q435-140 462.5-167.906 490-195.812 490-235v-490q0-65 45-110t110-45q65 0 110 45t45 110v615q0 12.75-8.675 21.375Q782.649-80 769.825-80 757-80 748.5-88.625T740-110v-615q0-39.188-27.867-67.094-27.867-27.906-67-27.906Q606-820 578-792.094 550-764.188 550-725v490q0 65-45.237 110Q459.525-80 396-80ZM261-295h26l28-296h-82l28 296Zm26-296h-54 82-28Z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#009e49" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#ce1126" d="M0 0h171v342H0z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#10338c" d="M0 0h513v342H0z"/><g fill="#FFF"><path d="M222.2 170.7c.3-.3.5-.6.8-.9-.2.3-.5.6-.8.9zM188 212.6l11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zM385.9 241.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM337.3 125.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zM385.9 58.9l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zM428.4 108.6l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"/><path d="M254.8 0v30.6l-45.1 25.1h45.1V115h-59.1l59.1 32.8v22.9h-26.7l-73.5-40.9v40.9H99v-48.6l-87.4 48.6H-1.2v-30.6L44 115H-1.2V55.7h59.1L-1.2 22.8V0h26.7L99 40.8V0h55.6v48.6L242.1 0z"/></g><path fill="#D80027" d="M142.8 0h-32v69.3h-112v32h112v69.4h32v-69.4h112v-32h-112z"/><path fill="#0052B4" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><path fill="#FFF" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><g fill="#D80027"><path d="m154.6 115 100.2 55.7v-15.8L183 115zM70.7 115l-71.9 39.9v15.8L99 115z"/></g><path fill="#0052B4" d="M99 55.7-1.2 0v15.7l71.9 40z"/><path fill="#FFF" d="M99 55.7-1.2 0v15.7l71.9 40z"/><g fill="#D80027"><path d="M99 55.7-1.2 0v15.7l71.9 40zM183 55.7l71.8-40V0L154.6 55.7z"/></g></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fdda25" d="M0 0h513v342H0z"/><path d="M0 0h171v342H0z"/><path fill="#ef3340" d="M342 0h171v342H342z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#009b3a" d="M0 0h513v342H0z"/><path fill="#fedf00" d="m256.5 19.3 204.9 151.4L256.5 322 50.6 170.7z"/><circle fill="#FFF" cx="256.5" cy="171" r="80.4"/><path fill="#002776" d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zM334.9 186c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="red"><path d="M0 0h142v342H0zM371 0h142v342H371zM306.5 206l50.4-25.2-25.2-12.6V143l-50.4 25.2 25.2-50.4h-25.2L256.1 80l-25.2 37.8h-25.2l25.2 50.4-50.4-25.2v25.2l-25.2 12.6 50.4 25.2-12.6 25.2h50.4V269h25.2v-37.8h50.4z"/></g></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 513 342"><path fill="red" d="M0 85.337h513v342H0z"/><path fill="#FFF" d="M356.174 222.609h-66.783v-66.783h-66.782v66.783h-66.783v66.782h66.783v66.783h66.782v-66.783h66.783z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg viewBox="0 0.5 21 14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M0 0h21v15H0z"/><path fill="#ee1c25" d="M0 0h21v15H0z"/><path d="M12 7.19c-.798-.5-1 .409-1 0 0-.828.895-1.5 2-1.5s2 .672 2 1.5c-.949 0-1.044.5-1.5.5-.56 0-.702 0-1.5-.5zM13.25 7a.25.25 0 1 0 0-.5.25.25 0 0 0 0 .5zm-1.81 1.962c.228-.913-.698-.824-.31-.95.788-.257 1.703.387 2.045 1.438.341 1.05-.021 2.11-.809 2.366-.293-.903-.798-.838-.939-1.272-.173-.533-.217-.668.012-1.582zm.566 1.13a.25.25 0 1 0 .476-.154.25.25 0 0 0-.476.154zM9.58 8.977c.94-.065.57-.919.81-.588.486.67.157 1.74-.737 2.389-.894.65-2.013.632-2.5-.038.768-.558.55-1.018.92-1.286.453-.33.568-.413 1.507-.477zm-.899.888a.25.25 0 1 0 .294.405.25.25 0 0 0-.294-.405zm.312-2.652c.351.874 1.049.258.809.588-.487.67-1.606.687-2.5.038-.894-.65-1.223-1.719-.736-2.39.767.559 1.138.21 1.507.478.453.33.568.413.92 1.286zm-1.124-.58a.25.25 0 1 0-.293.404.25.25 0 0 0 .293-.404zm2.619-.524c-.722.605.08 1.078-.309.951-.788-.256-1.15-1.315-.809-2.365.342-1.05 1.257-1.695 2.045-1.439-.293.903.153 1.147.012 1.581-.173.533-.217.668-.939 1.272zm.205-1.247a.25.25 0 1 0-.475-.155.25.25 0 0 0 .475.155z" fill="#FFF"/></g></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512v341.333H0z"/><path fill="#E00" d="M0 85.333h512V256H0z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M341.334 85.33H0v341.332h512V85.33z"/><path fill="#6DA544" d="M0 85.333h170.663V426.67H0z"/><path fill="#D80027" d="M341.337 85.333H512V426.67H341.337z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><circle fill="#D80027" cx="256" cy="255.994" r="96"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFDA44" d="M0 85.331h512v341.326H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path fill="#D80027" d="M0 85.337h512V256H0z"/><g fill="#FFF"><path d="M83.478 170.666c0-24.865 17.476-45.637 40.812-50.734a52.059 52.059 0 0 0-11.13-1.208c-28.688 0-51.942 23.254-51.942 51.941s23.255 51.942 51.942 51.942c3.822 0 7.543-.425 11.13-1.208-23.336-5.095-40.812-25.867-40.812-50.733zM150.261 122.435l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921z"/><path d="m121.344 144.696 3.683 11.337h11.921l-9.645 7.007 3.684 11.337-9.643-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921zM179.178 144.696l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.685-11.337-9.645-7.007h11.921zM168.047 178.087l3.684 11.337h11.921l-9.644 7.007 3.684 11.337-9.645-7.006-9.643 7.006 3.684-11.337-9.644-7.007h11.92zM132.474 178.087l3.683 11.337h11.921l-9.644 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.684-11.337-9.644-7.007h11.92z"/></g></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.337h512v341.326H0z"/><path fill="#0052B4" d="M0 85.337h256V256H0z"/><path fill="#FFF" d="M186.435 170.669 162.558 181.9l12.714 23.125-25.927-4.961-3.286 26.192L128 206.993l-18.06 19.263-3.285-26.192-25.927 4.96 12.714-23.125-23.877-11.23 23.877-11.231-12.714-23.125 25.927 4.96 3.286-26.192L128 134.344l18.06-19.263 3.285 26.192 25.928-4.96-12.715 23.125z"/><circle fill="#0052B4" cx="128" cy="170.674" r="29.006"/><path fill="#FFF" d="M128 190.06c-10.692 0-19.391-8.7-19.391-19.391 0-10.692 8.7-19.391 19.391-19.391 10.692 0 19.391 8.7 19.391 19.391 0 10.691-8.699 19.391-19.391 19.391z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#D80027"><path d="M0 0h513v26.3H0zM0 52.6h513v26.3H0zM0 105.2h513v26.3H0zM0 157.8h513v26.3H0zM0 210.5h513v26.3H0zM0 263.1h513v26.3H0zM0 315.7h513V342H0z"/></g><path fill="#2E52B2" d="M0 0h256.5v184.1H0z"/><g fill="#FFF"><path d="m47.8 138.9-4-12.8-4.4 12.8H26.2l10.7 7.7-4 12.8 10.9-7.9 10.6 7.9-4.1-12.8 10.9-7.7zM104.1 138.9l-4.1-12.8-4.2 12.8H82.6l10.7 7.7-4 12.8 10.7-7.9 10.8 7.9-4-12.8 10.7-7.7zM160.6 138.9l-4.3-12.8-4 12.8h-13.5l11 7.7-4.2 12.8 10.7-7.9 11 7.9-4.2-12.8 10.7-7.7zM216.8 138.9l-4-12.8-4.2 12.8h-13.3l10.8 7.7-4 12.8 10.7-7.9 10.8 7.9-4.3-12.8 11-7.7zM100 75.3l-4.2 12.8H82.6L93.3 96l-4 12.6 10.7-7.8 10.8 7.8-4-12.6 10.7-7.9h-13.4zM43.8 75.3l-4.4 12.8H26.2L36.9 96l-4 12.6 10.9-7.8 10.6 7.8L50.3 96l10.9-7.9H47.8zM156.3 75.3l-4 12.8h-13.5l11 7.9-4.2 12.6 10.7-7.8 11 7.8-4.2-12.6 10.7-7.9h-13.2zM212.8 75.3l-4.2 12.8h-13.3l10.8 7.9-4 12.6 10.7-7.8 10.8 7.8-4.3-12.6 11-7.9h-13.5zM43.8 24.7l-4.4 12.6H26.2l10.7 7.9-4 12.7L43.8 50l10.6 7.9-4.1-12.7 10.9-7.9H47.8zM100 24.7l-4.2 12.6H82.6l10.7 7.9-4 12.7L100 50l10.8 7.9-4-12.7 10.7-7.9h-13.4zM156.3 24.7l-4 12.6h-13.5l11 7.9-4.2 12.7 10.7-7.9 11 7.9-4.2-12.7 10.7-7.9h-13.2zM212.8 24.7l-4.2 12.6h-13.3l10.8 7.9-4 12.7 10.7-7.9 10.8 7.9-4.3-12.7 11-7.9h-13.5z"/></g></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path d="M114.024 256.001 0 141.926v228.17z"/><path fill="#ffb915" d="M161.192 256 0 94.7v47.226l114.024 114.075L0 370.096v47.138z"/><path fill="#007847" d="M509.833 289.391c.058-.44.804-.878 2.167-1.318v-65.464H222.602L85.33 85.337H0V94.7L161.192 256 0 417.234v9.429h85.33l137.272-137.272h287.231z"/><path fill="#000c8a" d="M503.181 322.783H236.433l-103.881 103.88H512v-103.88z"/><path fill="#e1392d" d="M503.181 189.217H512V85.337H132.552l103.881 103.88z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none" opacity=".1"/><path d="M12 1c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg width="23" height="19" viewBox="0 0 23 19" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7C12 8.06087 11.5786 9.07828 10.8284 9.82843C10.0783 10.5786 9.06087 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 5.93913 4.42143 4.92172 5.17157 4.17157C5.92172 3.42143 6.93913 3 8 3V3ZM8 13C10.67 13 16 14.34 16 17V19H0V17C0 14.34 5.33 13 8 13ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#000000"/>
-</svg>
+++ /dev/null
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.99 2C6.47 2 2 6.48 2 12C2 17.52 6.47 22 11.99 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 11.99 2ZM18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.35 18.92 8ZM12 4.04C12.83 5.24 13.48 6.57 13.91 8H10.09C10.52 6.57 11.17 5.24 12 4.04ZM4.26 14C4.1 13.36 4 12.69 4 12C4 11.31 4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12C7.5 12.68 7.56 13.34 7.64 14H4.26ZM5.08 16H8.03C8.35 17.25 8.81 18.45 9.41 19.56C7.57 18.93 6.04 17.66 5.08 16ZM8.03 8H5.08C6.04 6.34 7.57 5.07 9.41 4.44C8.81 5.55 8.35 6.75 8.03 8ZM12 19.96C11.17 18.76 10.52 17.43 10.09 16H13.91C13.48 17.43 12.83 18.76 12 19.96ZM14.34 14H9.66C9.57 13.34 9.5 12.68 9.5 12C9.5 11.32 9.57 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.68 14.43 13.34 14.34 14ZM14.59 19.56C15.19 18.45 15.65 17.25 15.97 16H18.92C17.96 17.65 16.43 18.93 14.59 19.56ZM16.36 14C16.44 13.34 16.5 12.68 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.69 19.9 13.36 19.74 14H16.36Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1 1.27L2.28 0L21 18.72L19.73 20L15.73 16C15.9 16.31 16 16.64 16 17V19H0V17C0 14.34 5.33 13 8 13C9.77 13 12.72 13.59 14.5 14.77L10.12 10.39C9.5 10.78 8.78 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 6.22 4.22 5.5 4.61 4.88L1 1.27ZM8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7V7.17L7.83 3H8ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#9C0707"/>
-</svg>
+++ /dev/null
-<svg width="50" height="93" viewBox="0 0 50 93" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7628 73.8622C11.7925 73.9104 11.8196 73.9602 11.8441 74.0113L14.6714 78.9066C16.8218 82.5868 20.3392 85.2682 24.4576 86.3667C28.576 87.4652 32.9617 86.8917 36.6592 84.7713L36.7948 84.6967C40.4339 82.5266 43.0758 79.0148 44.1522 74.9168C45.2287 70.8188 44.6538 66.462 42.5511 62.7835L24.55 31.5952C23.4582 31.5595 22.4078 31.169 21.5579 30.4828C20.708 29.7966 20.1049 28.8521 19.84 27.7924C19.5751 26.7327 19.6627 25.6155 20.0897 24.61C20.5166 23.6046 21.2597 22.7657 22.2062 22.2204C23.1527 21.6752 24.2511 21.4533 25.3351 21.5883C26.419 21.7233 27.4295 22.208 28.2133 22.9688C28.9971 23.7296 29.5116 24.7251 29.6788 25.8046C29.8461 26.8841 29.6569 27.9886 29.1401 28.9509L47.148 60.1393C49.9521 65.0155 50.726 70.799 49.3027 76.2409C47.8795 81.6829 44.3731 86.3468 39.5407 89.2258C39.4104 89.3213 39.272 89.4052 39.1272 89.4767C34.2442 92.2178 28.4827 92.9402 23.0741 91.4895C17.6655 90.0389 13.0389 86.5302 10.183 81.7135C10.0968 81.587 10.0219 81.4531 9.95926 81.3135L7.24723 76.6284L7.16587 76.4996C6.84576 75.9775 6.33816 75.5975 5.747 75.4374C5.15584 75.2773 4.52584 75.3493 3.98601 75.6386C3.68462 75.8135 3.35172 75.9274 3.00632 75.9737C2.66092 76.02 2.30977 75.9978 1.97294 75.9084C1.63611 75.819 1.32019 75.6641 1.04321 75.4526C0.766237 75.2411 0.533626 74.9772 0.358668 74.6758C0.18371 74.3744 0.0698312 74.0415 0.0235283 73.6961C-0.0227746 73.3507 -0.000592648 72.9995 0.0888089 72.6627C0.17821 72.3259 0.333083 72.01 0.544578 71.733C0.756074 71.456 1.02005 71.2234 1.32144 71.0484C3.07661 70.0427 5.15809 69.7712 7.11251 70.2931C9.06693 70.8151 10.7359 72.0881 11.756 73.835L11.7628 73.8622ZM30.747 8.18523e-07C29.4452 -0.000742291 28.194 0.504523 27.2579 1.40909C26.3217 2.31366 25.7737 3.54669 25.7297 4.84775L15.6816 10.6515C15.6305 10.676 15.5808 10.7031 15.5325 10.7329C11.7676 12.916 9.0218 16.5028 7.89703 20.707C6.77225 24.9113 7.36024 29.3899 9.53211 33.1614L9.62703 33.3241L9.7084 33.4597L30.6249 69.6924C30.9206 70.2333 30.9936 70.8682 30.8283 71.462C30.6738 72.056 30.2913 72.5652 29.7639 72.8791C29.5615 72.992 29.3766 73.1336 29.2147 73.2994V73.2994C28.7619 73.7725 28.4992 74.3958 28.4767 75.0503C28.4542 75.7047 28.6734 76.3446 29.0927 76.8476C29.5119 77.3507 30.1017 77.6818 30.7495 77.7777C31.3973 77.8736 32.0577 77.7275 32.6047 77.3675C34.2343 76.341 35.4179 74.739 35.9204 72.8798C36.4229 71.0207 36.2074 69.0405 35.3168 67.333C35.2451 67.1605 35.1541 66.9968 35.0456 66.8448L14.2511 30.829L14.1697 30.6799L14.0748 30.5104C12.6177 27.9552 12.2276 24.9282 12.9893 22.0871C13.751 19.246 15.6029 16.8202 18.1428 15.3365L18.2784 15.2552L28.3197 9.45822C28.992 9.81663 29.7371 10.0172 30.4985 10.0448C31.2599 10.0723 32.0176 9.92613 32.714 9.61725C33.4105 9.30837 34.0275 8.84494 34.5182 8.2621C35.0088 7.67927 35.3604 6.99235 35.546 6.25343C35.7317 5.51451 35.7466 4.743 35.5897 3.99745C35.4328 3.2519 35.1081 2.55189 34.6404 1.95049C34.1726 1.3491 33.574 0.862131 32.8901 0.526529C32.2061 0.190926 31.4546 0.0154886 30.6927 0.0135535L30.747 8.18523e-07Z" fill="#F21B1B"/>
-</svg>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<svg viewBox="0 0 13 12" width="13" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M 7.543 12 L 7.543 10.937 C 8.651 10.617 9.557 10.003 10.26 9.094 C 10.963 8.186 11.314 7.154 11.314 6 C 11.314 4.846 10.603 3.712 9.906 2.798 C 9.209 1.884 8.663 1.371 7.543 1.063 L 7.543 0 C 8.96 0.32 10.114 1.037 11.006 2.151 C 11.897 3.266 12.343 4.549 12.343 6 C 12.343 7.451 11.897 8.734 11.006 9.849 C 10.114 10.963 8.96 11.68 7.543 12 Z M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
-</svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg width="624" height="750" viewBox="0 0 624 750" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M614.44 570.333C601.997 557.891 583.331 557.891 570.883 570.333L499.331 641.891V32.104C499.331 14.9947 485.331 0.99469 468.221 0.99469C451.112 0.99469 437.112 14.9947 437.112 32.104V641.877L365.555 570.32C353.112 557.877 334.445 557.877 321.997 570.32C309.555 582.763 309.555 601.429 321.997 613.877L446.44 738.32C447.997 739.877 449.549 741.429 451.107 742.987L452.664 744.544C454.221 744.544 454.221 746.101 455.773 746.101C457.331 746.101 457.331 746.101 458.883 747.659C460.44 747.659 460.44 747.659 461.992 749.216H468.216H474.44C475.997 749.216 475.997 749.216 477.549 747.659C479.107 747.659 479.107 747.659 480.659 746.101C482.216 746.101 482.216 744.544 483.768 744.544C483.768 744.544 485.325 744.544 485.325 742.987C486.883 741.429 488.435 739.877 489.992 738.32L614.435 613.877C626.888 601.435 626.888 582.768 614.44 570.325L614.44 570.333Z" fill="black"/>
-<path d="M303.333 134.773L174.224 5.664C172.667 5.664 172.667 4.10667 171.115 4.10667C169.557 4.10667 169.557 2.54934 168.005 2.54934C166.448 2.54934 166.448 2.54934 164.896 0.992004H161.787C158.667 0.997213 155.557 0.997213 150.891 0.997213H147.781C146.224 0.997213 146.224 0.997213 144.672 2.55455C143.115 2.55455 143.115 4.11188 141.563 4.11188C140.005 4.11188 140.005 5.66921 138.453 5.66921L9.34413 134.779C-3.09854 147.221 -3.09854 165.888 9.34413 178.336C21.7868 190.779 40.4535 190.779 52.9015 178.336L126 106.773V716.56C126 733.669 140 747.669 157.109 747.669C174.219 747.669 188.219 733.669 188.219 716.56L188.224 106.773L259.781 178.331C266 184.555 273.776 187.664 281.557 187.664C289.333 187.664 297.115 184.555 303.333 178.331C315.776 165.888 315.776 147.221 303.333 134.773V134.773Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M444-164q-27-11-40-41t2-59q8-15 40-80.5t70-145q38-79.5 74.5-155T643-754q3-7 10.5-10t15.5-1q8 2 13 9t3 15q-9 36-29.5 119T613-451.5q-22 87.5-41 159T547-206q-12 30-43 41.5t-60 .5Zm484-393q-13 13-31.5 13T864-556q-34-29-71-54t-71-41l23-90q54 24 98.5 55t84.5 66q14 12 14 30.5T928-557Zm-896 0q-13-13-13.5-32T32-620q92-84 207-132t241-48q24 0 54.5 2.5T594-790l-42 85q-16-2-34-3.5t-38-1.5q-108 0-204.5 41.5T96-556q-14 12-32.5 12T32-557Zm727 169q-13 13-30.5 13T696-387q-11-8-19-14t-14-11l22-92q16 9 34.5 22.5T759-450q14 12 14 30t-14 32Zm-558 0q-14-14-13-32.5t13-29.5q61-53 129-81.5T483-560l-45 93q-49 6-92.5 27T264-387q-15 12-32.5 12T201-388Z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9 4H8.5V3C8.5 1.62 7.38 0.5 6 0.5C4.62 0.5 3.5 1.62 3.5 3V4H3C2.45 4 2 4.45 2 5V10C2 10.55 2.45 11 3 11H9C9.55 11 10 10.55 10 10V5C10 4.45 9.55 4 9 4ZM4.45 3C4.45 2.145 5.145 1.45 6 1.45C6.855 1.45 7.55 2.145 7.55 3V4H4.45V3ZM8 8H6.5V9.5H5.5V8H4V7H5.5V5.5H6.5V7H8V8Z" fill="#FAFBFB"/>
-</svg>
+++ /dev/null
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 1C3.24 1 1 3.24 1 6C1 8.76 3.24 11 6 11C8.76 11 11 8.76 11 6C11 3.24 8.76 1 6 1ZM5.5 9.965C3.525 9.72 2 8.04 2 6C2 5.69 2.04 5.395 2.105 5.105L4.5 7.5V8C4.5 8.55 4.95 9 5.5 9V9.965ZM8.95 8.695C8.82 8.29 8.45 8 8 8H7.5V6.5C7.5 6.225 7.275 6 7 6H4V5H5C5.275 5 5.5 4.775 5.5 4.5V3.5H6.5C7.05 3.5 7.5 3.05 7.5 2.5V2.295C8.965 2.89 10 4.325 10 6C10 7.04 9.6 7.985 8.95 8.695Z" fill="#FAFBFB"/>
-</svg>
#include <ctime>
#include "about.h"
-#ifndef NO_VS
-#include "virtualstudio.h"
-#endif
#include "ui_qjacktrip.h"
#ifdef USE_WEAK_JACK
#include "weak_libjack.h"
#include "../Meter.h"
#include "../Reverb.h"
-QJackTrip::QJackTrip(QSharedPointer<Settings> settings, bool suppressCommandlineWarning,
- QWidget* parent)
+QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent)
: QMainWindow(parent)
+ , m_interface(interface)
, m_ui(new Ui::QJackTrip)
, m_netManager(new QNetworkAccessManager(this))
, m_statsDialog(new MessageDialog(this, QStringLiteral("Stats")))
, m_jackTripRunning(false)
, m_isExiting(false)
, m_exitSent(false)
- , m_suppressCommandlineWarning(suppressCommandlineWarning)
, m_hideWarning(false)
{
m_ui->setupUi(this);
- m_cliSettings = settings;
// Set up our debug window, and relay everything to our real cout.
std::cout.rdbuf(m_debugDialog->getOutputStream()->rdbuf());
QStringLiteral("(This is for JackTrip's inbuilt authentication system. To easily "
"connect to a Virtual Studio server, download a Virtual Studio "
"enabled version of JackTrip.)"));
+ m_ui->vsModeButton->setVisible(false);
#else
connect(m_ui->vsModeButton, &QPushButton::clicked, this,
&QJackTrip::virtualStudioMode);
+ m_ui->vsModeButton->setVisible(true);
#endif
connect(m_ui->autoPatchComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [=]() {
m_ui->autoPatchGroupBox->setVisible(false);
m_ui->requireAuthGroupBox->setVisible(false);
m_ui->backendWarningLabel->setVisible(false);
- m_ui->vsModeButton->setVisible(false);
m_ui->inputGroupBox->setVisible(false);
m_ui->outputGroupBox->setVisible(false);
QMainWindow::showEvent(event);
if (m_firstShow) {
QSettings settings;
- loadSettings(m_cliSettings.data());
+ loadSettings(&m_interface.getSettings());
// Display a warning about any ignored command line options.
- if (m_cliSettings->guiIgnoresArguments() && !m_suppressCommandlineWarning) {
+ if (m_interface.getSettings().guiIgnoresArguments()) {
QMessageBox msgBox;
msgBox.setText(
"You have supplied command line options that the GUI version of JackTrip "
}
}
-#ifndef NO_VS
-void QJackTrip::setVs(QSharedPointer<VirtualStudio> vs)
-{
- m_vs = vs;
- m_ui->vsModeButton->setVisible(!m_vs.isNull());
-}
-#endif
-
void QJackTrip::processFinished()
{
if (!m_jackTripRunning) {
return;
}
m_jackTripRunning = false;
-#ifdef __APPLE__
- m_noNap.enableNap();
-#endif
+ m_interface.enableNap();
m_ui->disconnectButton->setEnabled(false);
if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
m_udpHub.reset();
m_ui->addressComboBox->insertItem(0, serverAddress);
m_ui->addressComboBox->setCurrentIndex(0);
-#ifdef __APPLE__
- m_noNap.disableNap();
-#endif
+ m_interface.disableNap();
}
void QJackTrip::stop()
#ifndef NO_VS
void QJackTrip::virtualStudioMode()
{
- this->hide();
- m_vs->show();
- m_vs->toVirtualStudio();
+ m_interface.setMode(UserInterface::MODE_VS);
+ QSettings settings;
+ settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_VS);
}
#endif
std::cout.rdbuf(m_realCout.rdbuf());
std::cerr.rdbuf(m_realCerr.rdbuf());
}
+
+QCoreApplication* QJackTrip::createApplication(int& argc, char* argv[])
+{
+ // Turn on high DPI support.
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+
+ // Fix for display scaling like 125% or 150% on Windows
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
+#if defined(NO_VS) && defined(_WIN32)
+ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+ Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor);
+#else
+ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+#endif // NO_VS
+#endif // QT_VERSION
+
+ return new QApplication(argc, argv);
+}
#include "../JackTrip.h"
#include "../Settings.h"
#include "../UdpHubListener.h"
+#include "../UserInterface.h"
#include "messageDialog.h"
#include "vuMeter.h"
-#ifdef __APPLE__
-#include "NoNap.h"
-#endif
-
#ifdef RT_AUDIO
#include <QComboBox>
#endif
class QJackTrip;
} // namespace Ui
-#ifndef NO_VS
-class VirtualStudio;
-#endif
-
class QJackTrip : public QMainWindow
{
Q_OBJECT
public:
- explicit QJackTrip(QSharedPointer<Settings> settings,
- bool suppressCommandlineWarning = false,
- QWidget* parent = nullptr);
+ explicit QJackTrip(UserInterface& interface, QWidget* parent = nullptr);
~QJackTrip() override;
void closeEvent(QCloseEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void showEvent(QShowEvent* event) override;
-
-#ifndef NO_VS
- enum uiModeT { UNSET, VIRTUAL_STUDIO, STANDARD };
- void setVs(QSharedPointer<VirtualStudio> vs);
-#endif
+ static QCoreApplication* createApplication(int& argc, char* argv[]);
signals:
void signalExit();
JackTrip::hubConnectionModeT hubModeFromPatchType(patchTypeT patchType);
+ UserInterface& m_interface;
QScopedPointer<Ui::QJackTrip> m_ui;
QScopedPointer<UdpHubListener> m_udpHub;
QScopedPointer<JackTrip> m_jackTrip;
bool m_isExiting;
bool m_exitSent;
- QSharedPointer<Settings> m_cliSettings;
bool m_suppressCommandlineWarning;
float m_meterMax = 0.0;
QLabel m_autoQueueIndicator;
bool m_hideWarning;
bool m_firstShow = true;
-
-#ifndef NO_VS
- QSharedPointer<VirtualStudio> m_vs;
-#endif
-#ifdef __APPLE__
- NoNap m_noNap;
-#endif
};
#endif // QJACKTRIP_H
+++ /dev/null
-<RCC>
- <qresource prefix="qjacktrip">
- <file>about@2x.png</file>
- <file>about.png</file>
- <file>icon.png</file>
- </qresource>
- <qresource prefix="vs">
- <file>vs.qml</file>
- <file>FirstLaunch.qml</file>
- <file>Login.qml</file>
- <file>LearnMoreButton.qml</file>
- <file>Recommendations.qml</file>
- <file>Permissions.qml</file>
- <file>ChangeDevices.qml</file>
- <file>Studio.qml</file>
- <file>Browse.qml</file>
- <file>AudioSettings.qml</file>
- <file>Settings.qml</file>
- <file>Meter.qml</file>
- <file>MeterBars.qml</file>
- <file>Connected.qml</file>
- <file>CreateStudio.qml</file>
- <file>Failed.qml</file>
- <file>Setup.qml</file>
- <file>SectionHeading.qml</file>
- <file>Footer.qml</file>
- <file>VolumeSlider.qml</file>
- <file>DeviceControls.qml</file>
- <file>DeviceControlsGroup.qml</file>
- <file>DeviceRefreshButton.qml</file>
- <file>DeviceWarning.qml</file>
- <file>InfoTooltip.qml</file>
- <file>Web.qml</file>
- <file>WebView.qml</file>
- <file>WebEngine.qml</file>
- <file>WebNull.qml</file>
- <file>FeedbackSurvey.qml</file>
- <file>AppIcon.qml</file>
- <file>logo.svg</file>
- <file>wedge.svg</file>
- <file>wedge_inactive.svg</file>
- <file>private.svg</file>
- <file>public.svg</file>
- <file>join.svg</file>
- <file>leave.svg</file>
- <file>manage.svg</file>
- <file>speed.svg</file>
- <file>share.svg</file>
- <file>start.svg</file>
- <file>star.svg</file>
- <file>cog.svg</file>
- <file>mic.svg</file>
- <file>language.svg</file>
- <file>micoff.svg</file>
- <file>help.svg</file>
- <file>quiet.svg</file>
- <file>loud.svg</file>
- <file>refresh.svg</file>
- <file>ethernet.svg</file>
- <file>networkCheck.svg</file>
- <file>externalMic.svg</file>
- <file>check.svg</file>
- <file>warning.svg</file>
- <file>expand_less.svg</file>
- <file>expand_more.svg</file>
- <file>sentiment_very_dissatisfied.svg</file>
- <file>headphones.svg</file>
- <file>Prompt.svg</file>
- <file>network.svg</file>
- <file>video.svg</file>
- <file>close.svg</file>
- <file>jacktrip.png</file>
- <file>jacktrip white.png</file>
- <file>JTOriginal.png</file>
- <file>JTVS.png</file>
- <file>flags/AE.svg</file>
- <file>flags/AU.svg</file>
- <file>flags/BE.svg</file>
- <file>flags/BR.svg</file>
- <file>flags/CA.svg</file>
- <file>flags/CH.svg</file>
- <file>flags/DE.svg</file>
- <file>flags/FR.svg</file>
- <file>flags/GB.svg</file>
- <file>flags/HK.svg</file>
- <file>flags/ID.svg</file>
- <file>flags/IT.svg</file>
- <file>flags/JP.svg</file>
- <file>flags/RO.svg</file>
- <file>flags/SE.svg</file>
- <file>flags/SG.svg</file>
- <file>flags/TW.svg</file>
- <file>flags/US.svg</file>
- <file>flags/ZA.svg</file>
- <file>Poppins-Bold.ttf</file>
- <file>Poppins-Regular.ttf</file>
- </qresource>
-</RCC>
<string>JackTrip</string>
</property>
<property name="windowIcon">
- <iconset resource="qjacktrip.qrc">
- <normaloff>:/qjacktrip/icon.png</normaloff>:/qjacktrip/icon.png</iconset>
+ <iconset resource="images.qrc">
+ <normaloff>:/images/icon_32.png</normaloff>:/images/icon_32.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QGridLayout" name="gridLayout">
+++ /dev/null
-<RCC>
- <qresource prefix="qjacktrip">
- <file>about@2x.png</file>
- <file>about.png</file>
- <file>icon.png</file>
- </qresource>
-</RCC>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<svg viewBox="0 0 10.05 12" width="10.05" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
-</svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
-<path d="M12 20q-3.35 0-5.675-2.325Q4 15.35 4 12q0-3.35 2.325-5.675Q8.65 4 12 4q1.725 0 3.3.713 1.575.712 2.7 2.037V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2Q13.625 6 12 6 9.5 6 7.75 7.75T6 12q0 2.5 1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325Q14.75 20 12 20Z"/>
-</svg>
\ No newline at end of file
+++ /dev/null
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 13.5C9.67 13.5 7.69 14.96 6.89 17H17.11C16.31 14.96 14.33 13.5 12 13.5ZM7.82 12L8.88 10.94L9.94 12L11 10.94L9.94 9.88L11 8.82L9.94 7.76L8.88 8.82L7.82 7.76L6.76 8.82L7.82 9.88L6.76 10.94L7.82 12ZM11.99 2C6.47 2 2 6.47 2 12C2 17.53 6.47 22 11.99 22C17.51 22 22 17.53 22 12C22 6.47 17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20ZM16.18 7.76L15.12 8.82L14.06 7.76L13 8.82L14.06 9.88L13 10.94L14.06 12L15.12 10.94L16.18 12L17.24 10.94L16.18 9.88L17.24 8.82L16.18 7.76Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M20.3642 8.55102L19.1342 10.401C19.7274 11.5841 20.0178 12.8958 19.9794 14.2187C19.941 15.5416 19.575 16.8343 18.9142 17.981H5.05424C4.19541 16.4911 3.83954 14.7641 4.03938 13.0561C4.23923 11.348 4.98415 9.74984 6.16372 8.49844C7.34329 7.24705 8.89471 6.40906 10.588 6.10871C12.2813 5.80837 14.0262 6.06165 15.5642 6.83102L17.4142 5.60102C15.5307 4.39323 13.2966 3.85202 11.0691 4.06395C8.8417 4.27588 6.74969 5.22871 5.12773 6.77003C3.50578 8.31134 2.4476 10.3521 2.12246 12.5658C1.79732 14.7796 2.22399 17.0384 3.33424 18.981C3.50875 19.2833 3.75933 19.5346 4.06107 19.7101C4.36282 19.8855 4.70521 19.9789 5.05424 19.981H18.9042C19.2567 19.9824 19.6032 19.8907 19.9087 19.7151C20.2143 19.5395 20.468 19.2862 20.6442 18.981C21.5656 17.3849 22.028 15.5653 21.9804 13.723C21.9327 11.8807 21.3769 10.0873 20.3742 8.54102L20.3642 8.55102Z" fill="black"/>
-<path d="M10.5742 15.391C10.76 15.577 10.9806 15.7245 11.2234 15.8251C11.4662 15.9258 11.7264 15.9776 11.9892 15.9776C12.2521 15.9776 12.5123 15.9258 12.7551 15.8251C12.9979 15.7245 13.2185 15.577 13.4042 15.391L19.0642 6.90102L10.5742 12.561C10.3883 12.7468 10.2408 12.9673 10.1401 13.2101C10.0395 13.4529 9.98767 13.7132 9.98767 13.976C9.98767 14.2388 10.0395 14.4991 10.1401 14.7419C10.2408 14.9847 10.3883 15.2053 10.5742 15.391Z" fill="black"/>
-</svg>
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 17.27l5.17 3.12c.38.23.85-.11.75-.54l-1.37-5.88 4.56-3.95c.33-.29.16-.84-.29-.88l-6.01-.51-2.35-5.54c-.17-.41-.75-.41-.92 0L9.19 8.63l-6.01.51c-.44.04-.62.59-.28.88l4.56 3.95-1.37 5.88c-.1.43.37.77.75.54L12 17.27z"/></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#000000"><path d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z" /></svg>
\ No newline at end of file
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H3V5h18v11z"/></svg>
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 virtualstudio.cpp
- * \author Matt Horton, based on code by Aaron Wyatt
- * \date March 2022
- */
-
-#include "virtualstudio.h"
-
-#include <QDebug>
-#include <QDesktopServices>
-#include <QMessageBox>
-#include <QQmlContext>
-#include <QQmlEngine>
-#include <QSettings>
-#include <QSslSocket>
-#include <QSysInfo>
-#include <algorithm>
-#include <iostream>
-
-// TODO: remove me; including this to work-around this bug
-// https://bugreports.qt.io/browse/QTBUG-55199
-#include <QSvgGenerator>
-
-#include "../JackTrip.h"
-#include "../Settings.h"
-#include "../jacktrip_globals.h"
-#include "WebSocketTransport.h"
-#include "about.h"
-#include "qjacktrip.h"
-#include "vsApi.h"
-#include "vsAudio.h"
-#include "vsAuth.h"
-#include "vsDevice.h"
-#include "vsWebSocket.h"
-
-#ifdef __APPLE__
-#include "vsMacPermissions.h"
-#else
-#include "vsPermissions.h"
-#endif
-
-#ifdef _WIN32
-#include <wingdi.h>
-#endif
-
-VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
- : QObject(parent)
- , m_audioConfigPtr(
- new VsAudio(this)) // this needs to be constructed before loadSettings()
- , m_showFirstRun(firstRun)
-{
- // load or initialize persisted settings
- loadSettings();
-
- // TODO: remove me; this is a hack for this bug
- // https://bugreports.qt.io/browse/QTBUG-55199
- QSvgGenerator svgImageHack;
-
- // use a singleton 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_networkAccessManagerPtr));
- m_api->setApiHost(PROD_API_HOST);
- if (m_testMode) {
- m_api->setApiHost(TEST_API_HOST);
- }
-
- // instantiate auth
- 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, [=]() {
- m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
- });
- connect(m_auth.data(), &VsAuth::fetchUserInfoFailed, this, [=]() {
- m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
- });
- connect(m_auth.data(), &VsAuth::deviceCodeExpired, this, [=]() {
- m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
- });
-
- m_webChannelServer.reset(new QWebSocketServer(
- QStringLiteral("Qt6 Virtual Studio Server"), QWebSocketServer::NonSecureMode));
- connect(m_webChannelServer.data(), &QWebSocketServer::newConnection, this, [=]() {
- m_webChannel->connectTo(
- new WebSocketTransport(m_webChannelServer->nextPendingConnection()));
- });
-
- m_webChannel.reset(new QWebChannel());
- m_webChannel->registerObject(QStringLiteral("virtualstudio"), this);
-
- // Load our font for our qml interface
- QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf"));
- QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf"));
-
- // Set our font scaling to convert points to pixels
- m_fontScale = float(4.0 / 3.0);
-
- // Initialize timer needed for network outage indicator
- m_networkOutageTimer.setTimerType(Qt::CoarseTimer);
- m_networkOutageTimer.setSingleShot(true);
- m_networkOutageTimer.setInterval(5000);
- m_networkOutageTimer.callOnTimeout([&]() {
- if (m_devicePtr.isNull())
- return;
- m_devicePtr->setNetworkOutage(false);
- emit updatedNetworkOutage(false);
- });
-
- if ((m_uiMode == QJackTrip::UNSET && vsFtux())
- || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) {
- m_windowState = QStringLiteral("login");
- }
-
- // register QML types
- qmlRegisterType<VsServerInfo>("org.jacktrip.jacktrip", 1, 0, "VsServerInfo");
-
- // setup QML view
- m_view.engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
- this);
- m_view.engine()->rootContext()->setContextProperty(QStringLiteral("auth"),
- m_auth.get());
- m_view.engine()->rootContext()->setContextProperty(QStringLiteral("audio"),
- m_audioConfigPtr.get());
- m_view.engine()->rootContext()->setContextProperty(
- QStringLiteral("permissions"),
- QVariant::fromValue(&m_audioConfigPtr->getPermissions()));
- m_view.setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml")));
- m_view.setMinimumSize(QSize(800, 640));
- // m_view.setMaximumSize(QSize(696, 577));
- m_view.setResizeMode(QQuickView::SizeRootObjectToView);
- m_view.resize(800 * m_uiScale, 640 * m_uiScale);
-
- // Connect our timers
- connect(&m_refreshTimer, &QTimer::timeout, this, [&]() {
- emit periodicRefresh();
- });
- connect(&m_heartbeatTimer, &QTimer::timeout, this, &VirtualStudio::sendHeartbeat,
- Qt::QueuedConnection);
-
- // QueuedConnection since refreshFinished is sometimes signaled from a network reply
- // thread
- connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio,
- Qt::QueuedConnection);
-
- // handle audio config errors
- connect(&m_audioConfigPtr->getWorker(), &VsAudioWorker::signalError, this,
- &VirtualStudio::processError, Qt::QueuedConnection);
-
- // when connected to server, trigger UI modal when feedback is detected
- connect(m_audioConfigPtr.get(), &VsAudio::feedbackDetected, this,
- &VirtualStudio::detectedFeedbackLoop, Qt::QueuedConnection);
-
- // call exit() when the UI window is closed
- connect(&m_view, &VsQuickView::windowClose, this, &VirtualStudio::exit,
- Qt::QueuedConnection);
-
- if ((m_uiMode == QJackTrip::UNSET && vsFtux())
- || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) {
- login();
- }
-}
-
-void VirtualStudio::setStandardWindow(QSharedPointer<QJackTrip> window)
-{
- m_standardWindow = window;
-}
-
-void VirtualStudio::setCLISettings(QSharedPointer<Settings> settings)
-{
- m_cliSettings = settings;
-}
-
-void VirtualStudio::show()
-{
- if (m_checkSsl) {
- // Check our available SSL version
- QString sslVersion = QSslSocket::sslLibraryVersionString();
- // Important: this needs to be output with qDebug rather than to std::cout
- // otherwise it may get passed to an existing JackTrip instance in place of our
- // deeplink. (Need to find the root cause of this.)
- qDebug() << "SSL Library: " << sslVersion;
- if (sslVersion.isEmpty()) {
- QMessageBox msgBox;
- msgBox.setText(
- QStringLiteral("OpenSSL was not found. You will not be able to connect "
- "to the Virtual Studio server."));
- msgBox.setWindowTitle(QStringLiteral("SSL Error"));
- msgBox.exec();
- }
- m_checkSsl = false;
- }
-
- 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 support.jacktrip.com 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.raise(); // raise to top
- m_view.requestActivate(); // focus on window
-}
-
-int VirtualStudio::webChannelPort()
-{
- return m_webChannelPort;
-}
-
-bool VirtualStudio::showFirstRun()
-{
- return m_showFirstRun;
-}
-
-void VirtualStudio::setShowFirstRun(bool show)
-{
- if (m_showFirstRun == show)
- return;
- m_showFirstRun = show;
- emit showFirstRunChanged();
-}
-
-bool VirtualStudio::hasRefreshToken()
-{
- return !m_refreshToken.isEmpty();
-}
-
-QString VirtualStudio::versionString()
-{
- return QLatin1String(gVersion);
-}
-
-QString VirtualStudio::logoSection()
-{
- return m_logoSection;
-}
-
-QString VirtualStudio::connectedErrorMsg()
-{
- return m_connectedErrorMsg;
-}
-
-void VirtualStudio::setConnectedErrorMsg(const QString& msg)
-{
- if (m_connectedErrorMsg == msg)
- return;
- m_connectedErrorMsg = msg;
- emit connectedErrorMsgChanged();
-}
-
-bool VirtualStudio::networkOutage()
-{
- return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage();
-}
-
-QJsonObject VirtualStudio::regions()
-{
- return m_regions;
-}
-
-QJsonObject VirtualStudio::userMetadata()
-{
- return m_userMetadata;
-}
-
-QString VirtualStudio::connectionState()
-{
- return m_connectionState;
-}
-
-QJsonObject VirtualStudio::networkStats()
-{
- return m_networkStats;
-}
-
-QString VirtualStudio::updateChannel()
-{
- return m_updateChannel;
-}
-
-void VirtualStudio::setUpdateChannel(const QString& channel)
-{
- if (m_updateChannel == channel)
- return;
- m_updateChannel = channel;
- emit updateChannelChanged();
-}
-
-bool VirtualStudio::showInactive()
-{
- return m_showInactive;
-}
-
-void VirtualStudio::setShowInactive(bool inactive)
-{
- if (m_showInactive == inactive)
- return;
- m_showInactive = inactive;
- emit showInactiveChanged();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("ShowInactive"), m_showInactive);
- settings.endGroup();
-}
-
-bool VirtualStudio::showSelfHosted()
-{
- return m_showSelfHosted;
-}
-
-void VirtualStudio::setShowSelfHosted(bool selfHosted)
-{
- if (m_showSelfHosted == selfHosted)
- return;
- m_showSelfHosted = selfHosted;
- emit showSelfHostedChanged();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted);
- settings.endGroup();
-}
-
-bool VirtualStudio::showCreateStudio()
-{
- return m_showCreateStudio;
-}
-
-void VirtualStudio::setShowCreateStudio(bool createStudio)
-{
- if (m_showCreateStudio == createStudio)
- return;
- m_showCreateStudio = createStudio;
- emit showCreateStudioChanged();
-}
-
-bool VirtualStudio::showDeviceSetup()
-{
- return m_showDeviceSetup;
-}
-
-void VirtualStudio::setShowDeviceSetup(bool show)
-{
- if (m_showDeviceSetup == show)
- return;
- m_showDeviceSetup = show;
- emit showDeviceSetupChanged();
-}
-
-QString VirtualStudio::windowState()
-{
- return m_windowState;
-}
-
-void VirtualStudio::setWindowState(QString state)
-{
- if (m_windowState == state)
- return;
- m_windowState = state;
- emit windowStateUpdated();
-}
-
-QString VirtualStudio::apiHost()
-{
- return m_apiHost;
-}
-
-void VirtualStudio::setApiHost(QString host)
-{
- if (m_apiHost == host)
- return;
- m_apiHost = host;
- emit apiHostChanged();
-}
-
-bool VirtualStudio::vsFtux()
-{
- return m_vsFtux;
-}
-
-bool VirtualStudio::isExiting()
-{
- return m_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);
- 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());
- return;
-}
-
-bool VirtualStudio::showWarnings()
-{
- return m_showWarnings;
-}
-
-void VirtualStudio::setShowWarnings(bool show)
-{
- if (m_showWarnings == show)
- return;
- m_showWarnings = show;
- emit showWarningsChanged();
-}
-
-float VirtualStudio::fontScale()
-{
- return m_fontScale;
-}
-
-float VirtualStudio::uiScale()
-{
- return m_uiScale;
-}
-
-void VirtualStudio::setUiScale(float scale)
-{
- if (scale == m_uiScale)
- return;
- m_uiScale = scale;
- emit uiScaleChanged();
-}
-
-bool VirtualStudio::darkMode()
-{
- return m_darkMode;
-}
-
-void VirtualStudio::setDarkMode(bool dark)
-{
- if (dark == m_darkMode)
- return;
- m_darkMode = dark;
- emit darkModeChanged();
-}
-
-bool VirtualStudio::collapseDeviceControls()
-{
- return m_collapseDeviceControls;
-}
-
-void VirtualStudio::setCollapseDeviceControls(bool collapseDeviceControls)
-{
- if (m_collapseDeviceControls == collapseDeviceControls)
- return;
- m_collapseDeviceControls = collapseDeviceControls;
- emit collapseDeviceControlsChanged(collapseDeviceControls);
-}
-
-bool VirtualStudio::testMode()
-{
- return m_testMode;
-}
-
-void VirtualStudio::setTestMode(bool test)
-{
- if (m_testMode == test)
- return;
-
- QString userEmail = m_userMetadata[QStringLiteral("email")].toString();
- if (m_userMetadata.isEmpty() || userEmail == ""
- || !userEmail.endsWith("@jacktrip.org")) {
- qDebug() << "Not allowed";
- return;
- }
-
- // deregister app
- if (!m_devicePtr.isNull()) {
- m_devicePtr->removeApp();
- m_devicePtr->disconnect();
- m_devicePtr.reset();
- }
-
- m_testMode = test;
-
- // clear existing auth state
- m_auth->logout();
-
- // Clear existing registers - any existing instance data will be overwritten
- // when m_auth->authenticate finishes and slotAuthSucceeded() is called again
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("TestMode"), m_testMode);
- settings.remove(QStringLiteral("RefreshToken"));
- settings.remove(QStringLiteral("UserId"));
- settings.endGroup();
-
- // stop timers, clear data, etc.
- resetState();
-
- // clear user data
- m_userMetadata = QJsonObject();
- m_userId.clear();
-
- // re-run authentication. This should not require another browser flow since
- // we're starting with the existing refresh token
- m_auth->authenticate(m_refreshToken);
- emit testModeChanged();
-}
-
-QString VirtualStudio::studioToJoin()
-{
- return m_studioToJoin;
-}
-
-void VirtualStudio::setStudioToJoin(const QString& id)
-{
- if (m_studioToJoin == id)
- return;
- m_studioToJoin = id;
- emit studioToJoinChanged();
-}
-
-bool VirtualStudio::noUpdater()
-{
-#ifdef NO_UPDATER
- return true;
-#else
- return false;
-#endif
-}
-
-bool VirtualStudio::psiBuild()
-{
-#ifdef PSI
- return true;
-#else
- return false;
-#endif
-}
-
-QString VirtualStudio::failedMessage()
-{
- return m_failedMessage;
-}
-
-void VirtualStudio::joinStudio()
-{
- // nothing to do unless on setup or connected windows with studio to join
- if ((m_windowState != "setup" && m_windowState != "connected")
- || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty())
- return;
-
- // make sure we've retrieved a list of servers
- QMutexLocker locker(&m_refreshMutex);
- if (m_servers.isEmpty()) {
- // No servers yet. Making sure we have them.
- // getServerList emits refreshFinished which
- // will come back to this function.
- locker.unlock();
- getServerList(true);
- return;
- }
-
- // pop studioToJoin
- const QString targetId = m_studioToJoin;
- setStudioToJoin("");
-
- // stop audio if already running (settings or setup windows)
- m_audioConfigPtr->stopAudio(true);
-
- // find and populate data for current studio
- VsServerInfoPointer sPtr;
- for (const VsServerInfoPointer& s : m_servers) {
- if (s->id() == targetId) {
- sPtr = s;
- break;
- }
- }
- locker.unlock();
-
- if (sPtr.isNull()) {
- m_failedMessage = "Unable to find studio " + targetId;
- emit failedMessageChanged();
- emit failed();
- return;
- }
-
- m_currentStudio = *sPtr;
- emit currentStudioChanged();
-
- if (m_windowState == "setup") {
- m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate());
- m_audioConfigPtr->startAudio();
- return;
- }
-
- // m_windowState == "connected"
- connectToStudio();
-}
-
-void VirtualStudio::toStandard()
-{
- if (m_standardWindow.isNull())
- qDebug() << "Unable to switch modes: standard window is missing!";
-
- m_view.hide();
- m_standardWindow->show();
-
- QSettings settings;
- m_uiMode = QJackTrip::STANDARD;
- settings.setValue(QStringLiteral("UiMode"), m_uiMode);
-
- // stop timers, clear data, etc.
- resetState();
- setWindowState(QStringLiteral("start"));
- m_auth->logout();
-
- if (m_showFirstRun) {
- m_showFirstRun = false;
- emit showFirstRunChanged();
- }
-}
-
-void VirtualStudio::toVirtualStudio()
-{
- QSettings settings;
- m_uiMode = QJackTrip::VIRTUAL_STUDIO;
- settings.setValue(QStringLiteral("UiMode"), m_uiMode);
-
- if (m_windowState == "start") {
- setWindowState(QStringLiteral("login"));
- }
- if (m_windowState == "login") {
- login();
- }
-}
-
-void VirtualStudio::login()
-{
- if (m_refreshToken.isEmpty()) {
- m_auth->authenticate(QStringLiteral(""));
- } else {
- m_auth->authenticate(m_refreshToken);
- }
-}
-
-void VirtualStudio::logout()
-{
- // deregister app
- if (!m_devicePtr.isNull()) {
- m_devicePtr->removeApp();
- m_devicePtr->disconnect();
- m_devicePtr.reset();
- }
-
- QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout");
- QUrlQuery query;
- query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID);
- if (m_testMode) {
- query.addQueryItem(QStringLiteral("returnTo"),
- QStringLiteral("https://next-test.jacktrip.com/"));
- } else {
- query.addQueryItem(QStringLiteral("returnTo"),
- QStringLiteral("https://www.jacktrip.com/"));
- }
-
- logoutURL.setQuery(query);
- QDesktopServices::openUrl(logoutURL);
-
- m_auth->logout();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.remove(QStringLiteral("RefreshToken"));
- settings.remove(QStringLiteral("UserId"));
- settings.endGroup();
-
- // stop timers, clear data, etc.
- resetState();
-
- // clear user data
- m_refreshToken.clear();
- m_userMetadata = QJsonObject();
- m_userId.clear();
- emit hasRefreshTokenChanged();
-
- // reset window state
- setWindowState(QStringLiteral("login"));
- login(); // called to retrieve new code flow token
-}
-
-void VirtualStudio::refreshStudios(int index, bool signalRefresh)
-{
- getSubscriptions();
- getServerList(signalRefresh, index);
-}
-
-void VirtualStudio::loadSettings()
-{
- QSettings settings;
- m_uiMode = static_cast<QJackTrip::uiModeT>(
- settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt());
- setUpdateChannel(
- settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower());
-
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- m_refreshToken = settings.value(QStringLiteral("RefreshToken"), "").toString();
- m_userId = settings.value(QStringLiteral("UserId"), "").toString();
- m_testMode = settings.value(QStringLiteral("TestMode"), false).toBool();
- m_showInactive = settings.value(QStringLiteral("ShowInactive"), true).toBool();
- m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), false).toBool();
-
- // use setters to emit signals for these if they change; otherwise, the
- // user interface will not revert back after cancelling settings changes
- setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat());
- setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool());
- setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool());
- setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool());
- settings.endGroup();
-
- m_audioConfigPtr->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);
- settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup);
- settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings);
- settings.endGroup();
-
- m_audioConfigPtr->saveSettings();
-}
-
-void VirtualStudio::connectToStudio()
-{
- m_refreshTimer.stop();
-
- m_networkStats = QJsonObject();
- emit networkStatsChanged();
-
- m_onConnectedScreen = true;
-
- m_studioSocketPtr.reset(new VsWebSocket(
- QUrl(QStringLiteral("wss://%1/api/servers/%2?auth_code=%3")
- .arg(m_api->getApiHost(), m_currentStudio.id(), m_auth->accessToken())),
- m_auth->accessToken(), QString(), QString()));
- connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
- &VirtualStudio::handleWebsocketMessage);
- connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this,
- &VirtualStudio::restartStudioSocket);
- m_studioSocketPtr->openSocket();
-
- // Check if we have an address for our server
- if (m_currentStudio.status() != "Ready") {
- m_connectionState = QStringLiteral("Waiting...");
- emit connectionStateChanged();
- } else {
- completeConnection();
- }
-
- m_reconnectState = ReconnectState::NOT_RECONNECTING;
-}
-
-void VirtualStudio::completeConnection()
-{
- // sanity check
- if (m_currentStudio.id() == ""
- || m_currentStudio.status() == QStringLiteral("Disabled")) {
- processError("Studio session has ended");
- return;
- }
-
- // these shouldn't happen
- if (m_currentStudio.status() != "Ready") {
- processError("Studio session is not ready");
- return;
- }
- if (m_currentStudio.host().isEmpty()) {
- processError("Studio host is unknown");
- return;
- }
-
- // always connect with audio device controls open
- setCollapseDeviceControls(false);
-
- m_jackTripRunning = true;
- m_connectionState = QStringLiteral("Preparing audio...");
- emit connectionStateChanged();
- try {
- bool useRtAudio = m_audioConfigPtr->getUseRtAudio();
- std::string input = "";
- std::string output = "";
- int buffer_size = 0;
- int inputMixMode = -1;
- int baseInputChannel = 0;
- int numInputChannels = 2;
- int baseOutputChannel = 0;
- int numOutputChannels = 2;
-#ifdef RT_AUDIO
- if (useRtAudio) {
- // pre-populate device cache and validate first, if using rtaudio
- if (!m_audioConfigPtr->getDeviceModelsInitialized())
- m_audioConfigPtr->refreshDevices(true);
- // initialize jacktrip using audio settings
- input = m_audioConfigPtr->getInputDevice().toStdString();
- output = m_audioConfigPtr->getOutputDevice().toStdString();
- buffer_size = m_audioConfigPtr->getBufferSize();
- inputMixMode = m_audioConfigPtr->getInputMixMode();
- baseInputChannel = m_audioConfigPtr->getBaseInputChannel();
- numInputChannels = m_audioConfigPtr->getNumInputChannels();
- baseOutputChannel = m_audioConfigPtr->getBaseOutputChannel();
- 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 == 4 || buffer_strategy == 5) {
- buffer_strategy = 3;
- }
-
- // create a new JackTrip instance
- JackTrip* jackTrip = m_devicePtr->initJackTrip(
- useRtAudio, input, output, baseInputChannel, numInputChannels,
- baseOutputChannel, numOutputChannels, inputMixMode, buffer_size,
- buffer_strategy, &m_currentStudio);
- if (jackTrip == 0) {
- processError("Could not bind port");
- return;
- }
- jackTrip->setIOStatTimeout(m_cliSettings->getIOStatTimeout());
- m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate());
-
- // this passes ownership to JackTrip
- jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip));
-
- QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this,
- &VirtualStudio::connectionFinished, Qt::QueuedConnection);
- QObject::connect(jackTrip, &JackTrip::signalError, this,
- &VirtualStudio::processError, Qt::QueuedConnection);
- QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this,
- &VirtualStudio::receivedConnectionFromPeer,
- Qt::QueuedConnection);
- QObject::connect(jackTrip, &JackTrip::signalUdpWaitingTooLong, this,
- &VirtualStudio::udpWaitingTooLong, Qt::QueuedConnection);
-
- m_connectionState = QStringLiteral("Connecting...");
- emit connectionStateChanged();
- m_devicePtr->startJackTrip(m_currentStudio);
-
- // update device error messages and warnings based on latest results
- // this is necessary because we may have never loaded audio settings,
- // or the state may have changed via the connected change devices screen
- m_audioConfigPtr->setDevicesWarningMsg(jackTrip->getDevicesWarningMsg());
- m_audioConfigPtr->setDevicesErrorMsg(jackTrip->getDevicesErrorMsg());
- m_audioConfigPtr->setDevicesWarningHelpUrl(jackTrip->getDevicesWarningHelpUrl());
- m_audioConfigPtr->setDevicesErrorHelpUrl(jackTrip->getDevicesErrorHelpUrl());
- m_audioConfigPtr->setHighLatencyFlag(jackTrip->getHighLatencyFlag());
-
- } catch (const std::exception& e) {
- // Let the user know what our exception was.
- m_connectionState = QStringLiteral("JackTrip Error");
- emit connectionStateChanged();
-
- processError(QString::fromUtf8(e.what()));
- return;
- }
-
-#ifdef __APPLE__
- m_noNap.disableNap();
-#endif
-}
-
-void VirtualStudio::triggerReconnect(bool refresh)
-{
- if (!m_jackTripRunning || m_devicePtr.isNull()) {
- if (refresh)
- m_audioConfigPtr->refreshDevices(true);
- else
- m_audioConfigPtr->validateDevices();
- return;
- }
-
- // this needs to be synchronous to avoid both trying
- // to use the audio interfaces at the same time
- // note that connectionFinished() checks m_reconnectState
- // and uses that to update audio, then reconnect
- m_reconnectState = refresh ? ReconnectState::RECONNECTING_REFRESH
- : ReconnectState::RECONNECTING_VALIDATE;
- m_connectionState = QStringLiteral("Reconnecting...");
- emit connectionStateChanged();
-
- // keep device enabled while stopping jacktrip
- m_devicePtr->stopJackTrip(true);
-}
-
-void VirtualStudio::disconnect()
-{
- // stop jackrip if it's running
- if (m_jackTripRunning) {
- m_devicePtr->stopJackTrip(false);
- // persist any volume level or device changes
- m_audioConfigPtr->saveSettings();
- }
-
- m_connectionState = QStringLiteral("Disconnected");
- emit connectionStateChanged();
- setConnectedErrorMsg("");
-
- if (m_isExiting) {
- emit signalExit();
- return;
- }
-
- // if this occurs on the setup or settings screen (for example, due to an issue with
- // devices) then don't emit disconnected, as that would move you back to the "Browse"
- // screen
- if (m_onConnectedScreen) {
- m_onConnectedScreen = false;
- emit disconnected();
- // Refresh studios and restart timer
- refreshStudios(0, true);
- m_refreshTimer.start();
- }
-
- if (!m_jackTripRunning) {
- return;
- }
- m_jackTripRunning = false;
-
- if (!m_studioSocketPtr.isNull()) {
- m_studioSocketPtr->closeSocket();
- m_studioSocketPtr->disconnect();
- m_studioSocketPtr.reset();
- }
-
- // reset network statistics
- m_networkStats = QJsonObject();
-
- if (!m_currentStudio.id().isEmpty()) {
- emit openFeedbackSurveyModal(m_currentStudio.id());
- m_currentStudio.setId("");
- emit currentStudioChanged();
- }
-
-#ifdef __APPLE__
- m_noNap.enableNap();
-#endif
-}
-
-void VirtualStudio::createStudio()
-{
- setWindowState(QStringLiteral("create_studio"));
-}
-
-void VirtualStudio::editProfile()
-{
- QUrl url = QUrl(QStringLiteral("https://%1/profile").arg(m_api->getApiHost()));
- QDesktopServices::openUrl(url);
-}
-
-void VirtualStudio::showAbout()
-{
- About about;
- about.exec();
-}
-
-void VirtualStudio::openLink(const QString& link)
-{
- QUrl url = QUrl(link);
- QDesktopServices::openUrl(url);
-}
-
-void VirtualStudio::handleDeeplinkRequest(const QUrl& link)
-{
- // check link is valid
- QString studioId;
- if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) {
- qDebug() << "Ignoring invalid deeplink to" << link;
- return;
- }
- if (link.host() == QLatin1String("join")) {
- studioId = link.path().remove(0, 1);
- } else if (link.host().isEmpty() && link.path().startsWith("join/")) {
- studioId = link.path().remove(0, 5);
- } else {
- qDebug() << "Ignoring invalid deeplink to" << link;
- return;
- }
-
- // check if already connected (ignore)
- if (m_windowState == "connected" || m_windowState == "change_devices") {
- qDebug() << "Already connected; ignoring deeplink to" << link;
- return;
- }
-
- if (m_windowState == "setup"
- && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) {
- qDebug() << "Already preparing to connect; ignoring deeplink to" << link;
- return;
- }
-
- qDebug() << "Handling deeplink to" << link;
- setStudioToJoin(studioId);
- raiseToTop();
-
- // Switch to virtual studio mode, if necessary
- // Note that this doesn't change the startup preference
- if (m_uiMode != QJackTrip::VIRTUAL_STUDIO) {
- m_standardWindow->hide();
- if (m_windowState == "start") {
- setWindowState(QStringLiteral("login"));
- }
- if (m_windowState == "login") {
- login();
- }
- }
-
- // automatically navigate if on certain window screens
- // note that the studio creation happens inside of the web view,
- // and the app doesn't really know anything about it. we depend
- // on the web app triggering a deep link join event, which is
- // handled here. it's unlikely that the new studio has been
- // noticed yet, so we don't join right away; otherwise we'd just
- // get an unknown studio error. instead, we trigger a refresh and
- // rely on it to kick off the join afterwards.
- if (m_windowState == "browse" || m_windowState == "create_studio"
- || m_windowState == "settings" || m_windowState == "setup"
- || m_windowState == "failed") {
- if (showDeviceSetup()) {
- setWindowState("setup");
- } else {
- setWindowState("connected");
- }
- refreshStudios(0, true);
- }
-
- // otherwise, assume we are on setup screens and can let the normal flow handle it
-}
-
-void VirtualStudio::exit()
-{
- // if multiple close events are received, emit the signalExit to force-quit the app
- if (m_isExiting) {
- emit signalExit();
- }
-
- // triggering isExitingChanged will force any WebEngine things to close properly
- m_isExiting = true;
- emit isExitingChanged();
-
- // stop timers, clear data, etc.
- resetState();
-
- if (m_onConnectedScreen) {
- // manually disconnect on self-managed studios
- if (!m_currentStudio.id().isEmpty() && !m_currentStudio.isManaged()) {
- disconnect();
- }
- } else {
- emit signalExit();
- }
-}
-
-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) {
- m_apiHost = TEST_API_HOST;
- }
- m_api->setApiHost(m_apiHost);
-
- // Get refresh token and userId
- m_refreshToken = m_auth->refreshToken();
- m_userId = m_auth->userId();
- emit hasRefreshTokenChanged();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken);
- settings.setValue(QStringLiteral("UserId"), m_userId);
- settings.endGroup();
-
- // initialize new VsDevice and wire up signals/slots before registering app
- m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr));
- connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this,
- &VirtualStudio::updatedStats);
- connect(m_audioConfigPtr.get(), &VsAudio::updatedInputVolume, m_devicePtr.get(),
- &VsDevice::syncDeviceSettings);
- connect(m_audioConfigPtr.get(), &VsAudio::updatedInputMuted, m_devicePtr.get(),
- &VsDevice::syncDeviceSettings);
- connect(m_audioConfigPtr.get(), &VsAudio::updatedOutputVolume, m_devicePtr.get(),
- &VsDevice::syncDeviceSettings);
- connect(m_audioConfigPtr.get(), &VsAudio::updatedMonitorVolume, m_devicePtr.get(),
- &VsDevice::syncDeviceSettings);
-
- m_devicePtr->registerApp();
-
- if (!m_webChannelServer->listen(QHostAddress::LocalHost)) {
- // shouldn't happen
- std::cout << "ERROR: Failed to start server!" << std::endl;
- }
- m_webChannelPort = m_webChannelServer->serverPort();
- emit webChannelPortChanged(m_webChannelPort);
- std::cout << "QWebChannel listening on port: " << m_webChannelPort << std::endl;
-
- getRegions();
- getUserMetadata();
- getSubscriptions();
- getServerList(false);
-}
-
-void VirtualStudio::connectionFinished()
-{
- if (!m_devicePtr.isNull()
- && (m_reconnectState == ReconnectState::RECONNECTING_VALIDATE
- || m_reconnectState == ReconnectState::RECONNECTING_REFRESH)) {
- if (m_devicePtr->hasTerminated()) {
- if (m_reconnectState == ReconnectState::RECONNECTING_REFRESH) {
- m_audioConfigPtr->refreshDevices(true);
- } else {
- m_audioConfigPtr->validateDevices(true);
- }
- connectToStudio();
- }
- return;
- }
- m_reconnectState = ReconnectState::NOT_RECONNECTING;
-
- // use disconnect function to handle reset of all internal flags and timers
- disconnect();
-}
-
-void VirtualStudio::processError(const QString& errorMessage)
-{
- static const QString RtAudioErrorMsg = QStringLiteral("RtAudio Error");
- static const QString JackAudioErrorMsg =
- QStringLiteral("The Jack server was shut down");
-
- const bool shouldSwitchToRtAudio =
- (errorMessage == QLatin1String("Maybe the JACK server is not running?"));
-
- QMessageBox msgBox;
- if (shouldSwitchToRtAudio) {
- // Report the other end quitting as a regular occurance rather than an error.
- msgBox.setText("The JACK server is not running. Switching back to RtAudio.");
- msgBox.setWindowTitle(QStringLiteral("No JACK server"));
- } else if (errorMessage == QLatin1String("Peer Stopped")) {
- // Report the other end quitting as a regular occurance rather than an error.
- msgBox.setText("The Studio has been stopped.");
- msgBox.setWindowTitle(QStringLiteral("Disconnected"));
- } else if (errorMessage.startsWith(RtAudioErrorMsg)) {
- if (errorMessage.length() > RtAudioErrorMsg.length() + 2) {
- const QString details(errorMessage.sliced(RtAudioErrorMsg.length() + 2));
- if (details.contains(QStringLiteral("device was disconnected"))
- || details.contains(
- QStringLiteral("Unable to retrieve capture buffer"))) {
- msgBox.setText(QStringLiteral("Your audio interface was disconnected."));
- } else {
- msgBox.setText(details);
- }
- } else {
- msgBox.setText(errorMessage);
- }
- msgBox.setWindowTitle(QStringLiteral("Audio Interface Error"));
- } else if (errorMessage.startsWith(JackAudioErrorMsg)) {
- if (errorMessage.length() > JackAudioErrorMsg.length() + 2) {
- msgBox.setText(errorMessage.sliced(JackAudioErrorMsg.length() + 2));
- } else {
- msgBox.setText(QStringLiteral("The JACK Audio Server was stopped."));
- }
- msgBox.setWindowTitle(QStringLiteral("Jack Audio Error"));
- } else {
- msgBox.setText(QStringLiteral("Error: ").append(errorMessage));
- msgBox.setWindowTitle(QStringLiteral("Doh!"));
- }
- msgBox.exec();
-
- if (m_jackTripRunning)
- connectionFinished();
-}
-
-void VirtualStudio::receivedConnectionFromPeer()
-{
- // Connect via API
- m_connectionState = QStringLiteral("Connected");
- emit connectionStateChanged();
- std::cout << "Received connection" << std::endl;
- emit connected();
-}
-
-void VirtualStudio::handleWebsocketMessage(const QString& msg)
-{
- QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object();
- QString serverStatus = serverState[QStringLiteral("status")].toString();
- bool serverEnabled = serverState[QStringLiteral("enabled")].toBool();
- QString serverCloudId = serverState[QStringLiteral("cloudId")].toString();
-
- // server notifications are also transmitted along this websocket, so ignore data if
- // it contains "message"
- QString message = serverState[QStringLiteral("message")].toString();
- if (!message.isEmpty()) {
- return;
- }
- if (m_currentStudio.id() == "") {
- return;
- }
- m_currentStudio.setStatus(serverStatus);
- m_currentStudio.setEnabled(serverEnabled);
- m_currentStudio.setCloudId(serverCloudId);
- if (!m_jackTripRunning) {
- if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) {
- m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString());
- m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt());
- m_currentStudio.setSessionId(
- serverState[QStringLiteral("sessionId")].toString());
- completeConnection();
- }
- }
-
- emit currentStudioChanged();
-}
-
-void VirtualStudio::restartStudioSocket()
-{
- if (m_onConnectedScreen) {
- if (!m_studioSocketPtr.isNull()) {
- m_studioSocketPtr->openSocket();
- }
- }
-}
-
-void VirtualStudio::updatedStats(const QJsonObject& stats)
-{
- QJsonObject newStats;
- for (int i = 0; i < stats.keys().size(); i++) {
- QString key = stats.keys().at(i);
- newStats.insert(key, stats[key].toDouble());
- }
-
- m_networkStats = newStats;
- emit networkStatsChanged();
- return;
-}
-
-void VirtualStudio::udpWaitingTooLong()
-{
- if (m_devicePtr.isNull())
- return;
- m_networkOutageTimer.start();
- m_devicePtr->setNetworkOutage(true);
- emit updatedNetworkOutage(true);
-}
-
-void VirtualStudio::sendHeartbeat()
-{
- if (!m_devicePtr.isNull() && m_connectionState != "Connecting..."
- && m_connectionState != "Preparing audio...") {
- m_devicePtr->sendHeartbeat();
- }
-}
-
-void VirtualStudio::resetState()
-{
- m_webChannelServer->close();
- m_refreshTimer.stop();
- m_heartbeatTimer.stop();
- m_startTimer.stop();
- m_networkOutageTimer.stop();
- m_firstRefresh = true;
-}
-
-void VirtualStudio::getServerList(bool signalRefresh, int index)
-{
- // only allow one thread to refresh at a time
- QMutexLocker refreshLock(&m_refreshMutex);
- if (m_refreshInProgress)
- return;
- m_refreshInProgress = true;
- refreshLock.unlock();
-
- // Get the serverId of the server at the top of our screen if we know it
- QString topServerId;
- if (index >= 0 && index < m_serverModel.count()) {
- topServerId = m_serverModel.at(index)->id();
- }
-
- QNetworkReply* reply = m_api->getServers();
- connect(
- reply, &QNetworkReply::finished, this, [&, reply, topServerId, signalRefresh]() {
- if (reply->error() != QNetworkReply::NoError) {
- if (signalRefresh) {
- emit refreshFinished(index);
- }
- std::cerr << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- QMutexLocker refreshLock(&m_refreshMutex);
- m_refreshInProgress = false;
- return;
- }
-
- QByteArray response = reply->readAll();
- QJsonDocument serverList = QJsonDocument::fromJson(response);
- reply->deleteLater();
- if (!serverList.isArray()) {
- if (signalRefresh) {
- emit refreshFinished(index);
- }
- std::cerr << "Error: Not an array" << std::endl;
- QMutexLocker refreshLock(&m_refreshMutex);
- m_refreshInProgress = false;
- return;
- }
-
- QJsonArray servers = serverList.array();
- // Divide our servers by category initially so that they're easier to sort
- QVector<VsServerInfoPointer> yourServers;
- QVector<VsServerInfoPointer> subServers;
- QVector<VsServerInfoPointer> pubServers;
- int skippedStudios = 0;
-
- QMutexLocker refreshLock(&m_refreshMutex); // protect m_servers
- m_servers.clear();
- for (int i = 0; i < servers.count(); i++) {
- if (servers.at(i)[QStringLiteral("type")].toString().contains(
- QStringLiteral("JackTrip"))) {
- QSharedPointer<VsServerInfo> serverInfo(new VsServerInfo(this));
- serverInfo->setIsAdmin(
- servers.at(i)[QStringLiteral("admin")].toBool());
- serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString());
- serverInfo->setHost(
- servers.at(i)[QStringLiteral("serverHost")].toString());
- serverInfo->setIsManaged(
- servers.at(i)[QStringLiteral("managed")].toBool());
- 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->setStreamId(
- servers.at(i)[QStringLiteral("streamId")].toString());
- serverInfo->setInviteKey(
- servers.at(i)[QStringLiteral("inviteKey")].toString());
- serverInfo->setCloudId(
- servers.at(i)[QStringLiteral("cloudId")].toString());
- serverInfo->setEnabled(
- servers.at(i)[QStringLiteral("enabled")].toBool());
- serverInfo->setIsOwner(
- servers.at(i)[QStringLiteral("owner")].toBool());
-
- // Always add servers to m_servers
- m_servers.append(serverInfo);
-
- // Only add servers to the model that we want to show
- if (serverInfo->isAdmin() || serverInfo->isOwner()) {
- if (filterStudio(*serverInfo)) {
- ++skippedStudios;
- } else {
- yourServers.append(serverInfo);
- serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
- }
- } else if (m_subscribedServers.contains(serverInfo->id())) {
- if (filterStudio(*serverInfo)) {
- ++skippedStudios;
- } else {
- subServers.append(serverInfo);
- serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
- }
- } else {
- if (!filterStudio(*serverInfo)) {
- pubServers.append(serverInfo);
- serverInfo->setSection(VsServerInfo::PUBLIC_STUDIOS);
- }
- // don't count public studios in skipped count
- }
- }
- }
- refreshLock.unlock();
-
- // sort studios in each section by name
- auto serverSorter = [](VsServerInfoPointer first,
- VsServerInfoPointer second) {
- return *first < *second;
- };
- std::sort(yourServers.begin(), yourServers.end(), serverSorter);
- std::sort(subServers.begin(), subServers.end(), serverSorter);
- std::sort(pubServers.begin(), pubServers.end(), serverSorter);
-
- // 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 (skippedStudios == 0) {
- // This is a new user
- setShowCreateStudio(true);
- } else {
- // This is not a new user. One or more studios were filtered.
- setShowCreateStudio(false);
- }
- } else {
- m_logoSection = QStringLiteral("Subscribed Studios");
- }
- } else {
- m_logoSection = QStringLiteral("Your Studios");
- }
- emit logoSectionChanged();
-
- m_serverModel.clear();
- for (const VsServerInfoPointer& s : yourServers) {
- m_serverModel.append(s.get());
- }
- for (const VsServerInfoPointer& s : subServers) {
- m_serverModel.append(s.get());
- }
- for (const VsServerInfoPointer& s : pubServers) {
- m_serverModel.append(s.get());
- }
- emit serverModelChanged();
- int index = -1;
- if (!topServerId.isEmpty()) {
- for (int i = 0; i < m_serverModel.count(); i++) {
- if (m_serverModel.at(i)->id() == topServerId) {
- index = i;
- break;
- }
- }
- }
- if (m_firstRefresh) {
- m_refreshTimer.setInterval(5000);
- m_refreshTimer.start();
- m_heartbeatTimer.setInterval(5000);
- m_heartbeatTimer.start();
- m_firstRefresh = false;
- }
- m_refreshInProgress = false;
- if (signalRefresh) {
- emit refreshFinished(index);
- }
- });
-}
-
-bool VirtualStudio::filterStudio(const VsServerInfo& serverInfo) const
-{
- // Return true if we want to filter the studio out of the display model
- bool activeStudio = serverInfo.status() == QLatin1String("Ready");
- bool hostedStudio = serverInfo.isManaged();
- if (!m_showSelfHosted && !hostedStudio) {
- return true;
- }
- if (!m_showInactive && !activeStudio) {
- return true;
- }
- return false;
-}
-
-void VirtualStudio::getSubscriptions()
-{
- if (m_userId.isEmpty()) {
- qDebug() << "Invalid user ID";
- return;
- }
- QNetworkReply* reply = m_api->getSubscriptions(m_userId);
- connect(reply, &QNetworkReply::finished, this, [&, reply]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- }
-
- QByteArray response = reply->readAll();
- QJsonDocument subscriptionList = QJsonDocument::fromJson(response);
- if (!subscriptionList.isArray()) {
- std::cout << "Error: Not an array" << std::endl;
- reply->deleteLater();
- return;
- }
- m_subscribedServers.clear();
- QJsonArray subscriptions = subscriptionList.array();
- for (int i = 0; i < subscriptions.count(); i++) {
- m_subscribedServers.insert(
- subscriptions.at(i)[QStringLiteral("serverId")].toString(), true);
- }
- reply->deleteLater();
- });
-}
-
-void VirtualStudio::getRegions()
-{
- QNetworkReply* reply = m_api->getRegions(m_userId);
- connect(reply, &QNetworkReply::finished, this, [&, reply]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- }
-
- m_regions = QJsonDocument::fromJson(reply->readAll()).object();
- emit regionsChanged();
- reply->deleteLater();
- });
-}
-
-void VirtualStudio::getUserMetadata()
-{
- QNetworkReply* reply = m_api->getUser(m_userId);
- connect(reply, &QNetworkReply::finished, this, [&, reply]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- }
-
- m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object();
- emit userMetadataChanged();
- reply->deleteLater();
- });
-}
-
-bool VirtualStudio::readyToJoin()
-{
- // FTUX shows warnings and device setup views
- // if any of these enabled, do not immediately join
- return m_windowState == "connected"
- && (m_connectionState == QStringLiteral("Waiting...")
- || m_connectionState == QStringLiteral("Disconnected"));
-}
-
-void VirtualStudio::detectedFeedbackLoop()
-{
- emit feedbackDetected();
-}
-
-VirtualStudio::~VirtualStudio()
-{
- QDesktopServices::unsetUrlHandler("jacktrip");
- // stop the audio worker thread before destructing other things
- m_audioConfigPtr.reset();
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 virtualstudio.h
- * \author Matt Horton, based on code by Aaron Wyatt
- * \date March 2022
- */
-
-#ifndef VIRTUALSTUDIO_H
-#define VIRTUALSTUDIO_H
-
-#include <QJsonObject>
-#include <QMap>
-#include <QMutex>
-#include <QNetworkAccessManager>
-#include <QScopedPointer>
-#include <QSharedPointer>
-#include <QString>
-#include <QStringList>
-#include <QTimer>
-#include <QUrl>
-#include <QVector>
-#include <QWebChannel>
-#include <QWebSocketServer>
-
-#include "../Settings.h"
-#include "qjacktrip.h"
-#include "vsConstants.h"
-#include "vsQuickView.h"
-#include "vsServerInfo.h"
-
-#ifdef __APPLE__
-#include "NoNap.h"
-#endif
-
-class JackTrip;
-class VsAudio;
-class VsApi;
-class VsAuth;
-class VsDevice;
-class VsWebSocket;
-
-typedef QSharedPointer<VsServerInfo> VsServerInfoPointer;
-
-class VirtualStudio : public QObject
-{
- Q_OBJECT
- Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged)
- Q_PROPERTY(bool showFirstRun READ showFirstRun WRITE setShowFirstRun NOTIFY
- showFirstRunChanged)
- Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged)
- Q_PROPERTY(QString versionString READ versionString CONSTANT)
- Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged)
- Q_PROPERTY(
- QString connectedErrorMsg READ connectedErrorMsg NOTIFY connectedErrorMsgChanged)
-
- Q_PROPERTY(
- QVector<VsServerInfo*> serverModel READ getServerModel NOTIFY serverModelChanged)
- Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged)
- Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY
- studioToJoinChanged)
- Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged)
- Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged)
- Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY
- 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(bool networkOutage READ networkOutage NOTIFY updatedNetworkOutage)
-
- Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
- updateChannelChanged)
- Q_PROPERTY(float fontScale READ fontScale CONSTANT)
- Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged)
- Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
- Q_PROPERTY(bool collapseDeviceControls READ collapseDeviceControls WRITE
- setCollapseDeviceControls NOTIFY collapseDeviceControlsChanged)
- Q_PROPERTY(bool testMode READ testMode WRITE setTestMode NOTIFY testModeChanged)
- Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY
- showDeviceSetupChanged)
- Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY
- showWarningsChanged)
- Q_PROPERTY(bool isExiting READ isExiting NOTIFY isExitingChanged)
- Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT)
- Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT)
- Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged)
- Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY
- windowStateUpdated)
- Q_PROPERTY(QString apiHost READ apiHost WRITE setApiHost NOTIFY apiHostChanged)
- Q_PROPERTY(bool vsFtux READ vsFtux CONSTANT)
- Q_PROPERTY(
- QStringList updateChannelComboModel READ getUpdateChannelComboModel CONSTANT)
-
- public:
- explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr);
- ~VirtualStudio() override;
-
- void setStandardWindow(QSharedPointer<QJackTrip> window);
- void setCLISettings(QSharedPointer<Settings> settings);
- void show();
- void raiseToTop();
-
- int webChannelPort();
- bool showFirstRun();
- void setShowFirstRun(bool show);
- bool hasRefreshToken();
- QString versionString();
- QString logoSection();
- QString connectedErrorMsg();
- void setConnectedErrorMsg(const QString& msg);
- const QVector<VsServerInfo*>& getServerModel() const { return m_serverModel; }
- VsServerInfo* currentStudio() { return &m_currentStudio; }
- QJsonObject regions();
- QJsonObject userMetadata();
- QString connectionState();
- QJsonObject networkStats();
- 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);
- bool darkMode();
- void setDarkMode(bool dark);
- bool collapseDeviceControls();
- void setCollapseDeviceControls(bool collapseDeviceControls);
- bool testMode();
- void setTestMode(bool test);
- QString studioToJoin();
- void setStudioToJoin(const QString& id);
- bool showDeviceSetup();
- void setShowDeviceSetup(bool show);
- bool showWarnings();
- void setShowWarnings(bool show);
- bool noUpdater();
- bool psiBuild();
- QString failedMessage();
- bool networkOutage();
- bool backendAvailable();
- QString windowState();
- QString apiHost();
- void setApiHost(QString host);
- bool vsFtux();
- bool isExiting();
- const QStringList& getUpdateChannelComboModel() const
- {
- return m_updateChannelOptions;
- }
-
- public slots:
- void toStandard();
- void toVirtualStudio();
- void login();
- void logout();
- void refreshStudios(int index, bool signalRefresh = false);
- void loadSettings();
- void saveSettings();
- void triggerReconnect(bool refresh);
- void createStudio();
- void editProfile();
- void showAbout();
- void openLink(const QString& url);
- void handleDeeplinkRequest(const QUrl& url);
- void udpWaitingTooLong();
- void setWindowState(QString state);
- void joinStudio();
- void disconnect();
- void collectFeedbackSurvey(QString serverId, int rating, QString message);
-
- signals:
- void failed();
- void connected();
- void disconnected();
- void refreshFinished(int index);
- void webChannelPortChanged(int webChannelPort);
- void showFirstRunChanged();
- void hasRefreshTokenChanged();
- void logoSectionChanged();
- void connectedErrorMsgChanged();
- void serverModelChanged();
- void currentStudioChanged();
- void regionsChanged();
- void userMetadataChanged();
- void showInactiveChanged();
- void showSelfHostedChanged();
- void showCreateStudioChanged();
- void connectionStateChanged();
- void networkStatsChanged();
- void updateChannelChanged();
- void showDeviceSetupChanged();
- void showWarningsChanged();
- void uiScaleChanged();
- void collapseDeviceControlsChanged(bool collapseDeviceControls);
- void newScale();
- void darkModeChanged();
- void testModeChanged();
- void signalExit();
- void periodicRefresh();
- void failedMessageChanged();
- void studioToJoinChanged();
- void updatedNetworkOutage(bool outage);
- void windowStateUpdated();
- void isExitingChanged();
- void apiHostChanged();
- void feedbackDetected();
- void openFeedbackSurveyModal(QString serverId);
-
- private slots:
- void slotAuthSucceeded();
- void receivedConnectionFromPeer();
- void handleWebsocketMessage(const QString& msg);
- void restartStudioSocket();
- void updatedStats(const QJsonObject& stats);
- void processError(const QString& errorMessage);
- void detectedFeedbackLoop();
- void sendHeartbeat();
- void connectionFinished();
- void exit();
-
- private:
- void resetState();
- void getServerList(bool signalRefresh = false, int index = -1);
- bool filterStudio(const VsServerInfo& serverInfo) const;
- void getSubscriptions();
- void getRegions();
- void getUserMetadata();
- bool readyToJoin();
- void connectToStudio();
- void completeConnection();
-
- private:
- enum ReconnectState {
- NOT_RECONNECTING = 0,
- RECONNECTING_VALIDATE,
- RECONNECTING_REFRESH
- };
-
- VsQuickView m_view;
- VsServerInfo m_currentStudio;
- QNetworkAccessManager* m_networkAccessManagerPtr;
- QSharedPointer<QJackTrip> m_standardWindow;
- QSharedPointer<Settings> m_cliSettings;
- QSharedPointer<VsAuth> m_auth;
- QSharedPointer<VsApi> m_api;
- QScopedPointer<VsDevice> m_devicePtr;
- QScopedPointer<VsWebSocket> m_studioSocketPtr;
- QSharedPointer<VsAudio> m_audioConfigPtr;
- QVector<VsServerInfoPointer> m_servers;
- QVector<VsServerInfo*> m_serverModel; //< qml doesn't like smart pointers
- QScopedPointer<QWebSocketServer> m_webChannelServer;
- QScopedPointer<QWebChannel> m_webChannel;
- QMap<QString, bool> m_subscribedServers;
- QJsonObject m_regions;
- QJsonObject m_userMetadata;
- QJsonObject m_networkStats;
- QTimer m_startTimer;
- QTimer m_refreshTimer;
- QTimer m_heartbeatTimer;
- QTimer m_networkOutageTimer;
- QMutex m_refreshMutex;
- QString m_studioToJoin;
- QString m_updateChannel;
- QString m_refreshToken;
- QString m_userId;
- QString m_apiHost = PROD_API_HOST;
- ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING;
- QJackTrip::uiModeT m_uiMode = QJackTrip::UNSET;
-
- bool m_firstRefresh = true;
- bool m_jackTripRunning = false;
- bool m_showFirstRun = false;
- bool m_checkSsl = true;
- bool m_refreshInProgress = false;
- bool m_onConnectedScreen = false;
- bool m_isExiting = false;
- bool m_showInactive = true;
- bool m_showSelfHosted = false;
- bool m_showCreateStudio = false;
- bool m_showDeviceSetup = true;
- bool m_showWarnings = true;
- bool m_darkMode = false;
- bool m_collapseDeviceControls = false;
- bool m_testMode = false;
- bool m_authenticated = false;
- float m_fontScale = 1;
- float m_uiScale = 1;
- uint32_t m_webChannelPort = 1;
-
- QString m_failedMessage = QStringLiteral("");
- QString m_windowState = QStringLiteral("start");
- QString m_connectedErrorMsg = QStringLiteral("");
- QString m_logoSection = QStringLiteral("Your Studios");
- QString m_connectionState = QStringLiteral("Waiting...");
- QStringList m_updateChannelOptions = {"Stable", "Edge"};
-
-#ifdef __APPLE__
- NoNap m_noNap;
-#endif
-
-#ifdef VS_FTUX
- bool m_vsFtux = true;
-#else
- bool m_vsFtux = false;
-#endif
-};
-
-#endif // VIRTUALSTUDIO_H
+++ /dev/null
-import QtQuick
-import QtQuick.Controls
-
-Rectangle {
- property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
- property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-
- color: backgroundColour
- state: virtualstudio.windowState
- anchors.fill: parent
-
- id: window
- states: [
- State {
- name: "start"
- PropertyChanges { target: startScreen; x: 0 }
- PropertyChanges { target: loginScreen; x: window.width; }
- PropertyChanges { target: recommendationsScreen; x: window.width }
- PropertyChanges { target: permissionsScreen; x: window.width }
- 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 }
- },
-
- State {
- name: "login"
- PropertyChanges { target: startScreen; x: -startScreen.width }
- PropertyChanges { target: loginScreen; x: 0; }
- PropertyChanges { target: recommendationsScreen; x: window.width }
- PropertyChanges { target: permissionsScreen; x: window.width }
- 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 }
- },
-
- State {
- name: "recommendations"
- PropertyChanges { target: loginScreen; x: -loginScreen.width }
- PropertyChanges { target: startScreen; x: -startScreen.width }
- PropertyChanges { target: recommendationsScreen; x: 0 }
- PropertyChanges { target: permissionsScreen; x: window.width }
- 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 }
- },
-
- State {
- name: "permissions"
- PropertyChanges { target: loginScreen; x: -loginScreen.width }
- PropertyChanges { target: startScreen; x: -startScreen.width }
- PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
- PropertyChanges { target: permissionsScreen; x: 0 }
- 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 }
- },
-
- State {
- name: "setup"
- 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: 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 }
- },
-
- State {
- name: "browse"
- 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: 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 }
- },
-
- State {
- name: "settings"
- 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: 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 }
- PropertyChanges { target: startScreen; x: -startScreen.width }
- PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
- PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
- 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 }
- },
-
- State {
- name: "change_devices"
- 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: 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 }
- },
-
- State {
- name: "failed"
- 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: -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 }
- }
- ]
-
- transitions: Transition {
- NumberAnimation { properties: "x"; duration: 500; easing.type: Easing.InOutQuad }
- }
-
- FirstLaunch {
- id: startScreen
- }
-
- Login {
- id: loginScreen
- }
-
- Recommendations {
- id: recommendationsScreen
- }
-
- Permissions {
- id: permissionsScreen
- }
-
- Browse {
- id: browseScreen
- }
-
- Setup {
- id: setupScreen
- }
-
- Settings {
- id: settingsScreen
- }
-
- Connected {
- id: connectedScreen
- }
-
- ChangeDevices {
- id: changeDevicesScreen
- }
-
- CreateStudio {
- id: createStudioScreen
- }
-
- Failed {
- id: failedScreen
- }
-
- onWidthChanged: {
- if (virtualstudio.windowState === "start") {
- startScreen.x = 0
- } else if (virtualstudio.windowState === "login") {
- loginScreen.x = 0
- } else if (virtualstudio.windowState === "recommendations") {
- recommendationsScreen.x = 0;
- } else if (virtualstudio.windowState === "permissions") {
- permissionsScreen.x = 0;
- } else if (virtualstudio.windowState === "setup") {
- setupScreen.x = 0
- } else if (virtualstudio.windowState === "browse") {
- 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") {
- changeDevicesScreen.x = 0
- } else if (virtualstudio.windowState === "failed") {
- failedScreen.x = 0
- }
- }
-
- onHeightChanged: {
- if (virtualstudio.windowState === "start") {
- startScreen.x = 0
- } else if (virtualstudio.windowState === "login") {
- loginScreen.x = 0
- } else if (virtualstudio.windowState === "recommendations") {
- recommendationsScreen.x = 0;
- } else if (virtualstudio.windowState === "permissions") {
- permissionsScreen.x = 0;
- } else if (virtualstudio.windowState === "setup") {
- setupScreen.x = 0
- } else if (virtualstudio.windowState === "browse") {
- 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") {
- changeDevicesScreen.x = 0
- } else if (virtualstudio.windowState === "failed") {
- failedScreen.x = 0
- }
- }
-
- Connections {
- target: auth
- function onAuthSucceeded() {
- if (virtualstudio.windowState !== "login") {
- // can happen on settings screen when switching between prod and test
- return;
- }
- if (virtualstudio.showWarnings) {
- virtualstudio.windowState = "recommendations";
- } else if (virtualstudio.studioToJoin === "") {
- virtualstudio.windowState = "browse";
- } else {
- virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
- virtualstudio.joinStudio();
- }
- }
- }
- Connections {
- target: virtualstudio
- function onConnected() {
- if (virtualstudio.windowState == "change_devices") {
- return;
- }
- virtualstudio.windowState = "connected";
- }
- function onFailed() {
- virtualstudio.windowState = "failed";
- }
- function onDisconnected() {
- virtualstudio.windowState = "browse";
- }
- }
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsApi.cpp
- * \author Dominick Hing
- * \date May 2023
- */
-
-#include "vsApi.h"
-
-#include "../jacktrip_globals.h"
-
-VsApi::VsApi(QNetworkAccessManager* networkAccessManager)
-{
- m_networkAccessManager = networkAccessManager;
-}
-
-QNetworkReply* VsApi::getAuth0UserInfo()
-{
- return get(QUrl("https://auth.jacktrip.org/userinfo"));
-}
-
-QNetworkReply* VsApi::getUser(const QString& userId)
-{
- return get(QUrl(QString("https://%1/api/users/%2").arg(m_apiHost, userId)));
-}
-
-QNetworkReply* VsApi::getServers()
-{
- return get(QUrl(QString("https://%1/api/servers").arg(m_apiHost)));
-}
-
-QNetworkReply* VsApi::getSubscriptions(const QString& userId)
-{
- return get(
- QUrl(QString("https://%1/api/users/%2/subscriptions").arg(m_apiHost, userId)));
-}
-
-QNetworkReply* VsApi::getRegions(const QString& userId)
-{
- return get(QUrl(QString("https://%1/api/users/%2/regions").arg(m_apiHost, userId)));
-}
-
-QNetworkReply* VsApi::getDevice(const QString& deviceId)
-{
- return get(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
-}
-
-QNetworkReply* VsApi::postDevice(const QByteArray& data)
-{
- return post(QUrl(QString("https://%1/api/devices").arg(m_apiHost)), data);
-}
-
-QNetworkReply* VsApi::postDeviceHeartbeat(const QString& deviceId, const QByteArray& data)
-{
- return post(
- QUrl(QString("https://%1/api/devices/%2/heartbeat").arg(m_apiHost, deviceId)),
- data);
-}
-
-QNetworkReply* VsApi::submitServerFeedback(const QString& serverId,
- const QByteArray& data)
-{
- return post(
- QUrl(QString("https://%1/api/servers/%2/feedback").arg(m_apiHost, serverId)),
- data);
-}
-
-QNetworkReply* VsApi::updateServer(const QString& serverId, const QByteArray& data)
-{
- return put(QUrl(QString("https://%1/api/servers/%2").arg(m_apiHost, serverId)), data);
-}
-
-QNetworkReply* VsApi::updateDevice(const QString& deviceId, const QByteArray& data)
-{
- return put(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)), data);
-}
-
-QNetworkReply* VsApi::deleteDevice(const QString& deviceId)
-{
- return deleteResource(
- QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
-}
-
-QNetworkReply* VsApi::get(const QUrl& url)
-{
- QNetworkRequest request = QNetworkRequest(url);
- request.setRawHeader(QByteArray("User-Agent"),
- QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
-
- QNetworkReply* reply = m_networkAccessManager->get(request);
- return reply;
-}
-
-QNetworkReply* VsApi::post(const QUrl& url, const QByteArray& data)
-{
- QNetworkRequest request = QNetworkRequest(url);
- request.setRawHeader(QByteArray("User-Agent"),
- QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
- request.setRawHeader(QByteArray("Content-Type"),
- QString("application/json").toUtf8());
-
- QNetworkReply* reply = m_networkAccessManager->post(request, data);
- return reply;
-}
-
-QNetworkReply* VsApi::put(const QUrl& url, const QByteArray& data)
-{
- QNetworkRequest request = QNetworkRequest(url);
- request.setRawHeader(QByteArray("User-Agent"),
- QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
- request.setRawHeader(QByteArray("Content-Type"),
- QString("application/json").toUtf8());
- QNetworkReply* reply = m_networkAccessManager->put(request, data);
- return reply;
-}
-
-QNetworkReply* VsApi::deleteResource(const QUrl& url)
-{
- QNetworkRequest request = QNetworkRequest(url);
- request.setRawHeader(QByteArray("User-Agent"),
- QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
-
- QNetworkReply* reply = m_networkAccessManager->deleteResource(request);
- return reply;
-}
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsApi.h
- * \author Dominick Hing
- * \date May 2023
- */
-
-#ifndef VSAPI_H
-#define VSAPI_H
-
-#include <QEventLoop>
-#include <QJsonParseError>
-#include <QMap>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QString>
-#include <QUrl>
-#include <QVariant>
-#include <iostream>
-
-class VsApi : public QObject
-{
- Q_OBJECT
-
- public:
- VsApi(QNetworkAccessManager* networkAccessManager);
- void setAccessToken(QString token) { m_accessToken = token; };
- void setApiHost(QString host) { m_apiHost = host; }
- QString getApiHost() { return m_apiHost; }
-
- QNetworkReply* getAuth0UserInfo();
- QNetworkReply* getUser(const QString& userId);
- QNetworkReply* getServers();
- QNetworkReply* getSubscriptions(const QString& userId);
- QNetworkReply* getRegions(const QString& userId);
- QNetworkReply* getDevice(const QString& deviceId);
-
- QNetworkReply* postDevice(const QByteArray& data);
- QNetworkReply* postDeviceHeartbeat(const QString& deviceId, const QByteArray& data);
- QNetworkReply* submitServerFeedback(const QString& serverId, const QByteArray& data);
-
- QNetworkReply* updateServer(const QString& serverId, const QByteArray& data);
- QNetworkReply* updateDevice(const QString& deviceId, const QByteArray& data);
-
- QNetworkReply* deleteDevice(const QString& deviceId);
-
- private:
- QNetworkReply* get(const QUrl& url);
- QNetworkReply* put(const QUrl& url, const QByteArray& data);
- QNetworkReply* post(const QUrl& url, const QByteArray& data);
- QNetworkReply* deleteResource(const QUrl& url);
-
- QString m_accessToken;
- QString m_apiHost;
- QNetworkAccessManager* m_networkAccessManager;
-};
-
-#endif // VSAPI_H
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsAudio.cpp
- * \author Matt Horton
- * \date September 2022
- */
-
-#include "vsAudio.h"
-
-#include <QDebug>
-#include <QEventLoop>
-#include <QJsonObject>
-#include <QSettings>
-#include <QThread>
-
-#ifdef USE_WEAK_JACK
-#include "weak_libjack.h"
-#endif
-
-#ifndef NO_JACK
-#include "../JackAudioInterface.h"
-#endif
-
-#ifdef __APPLE__
-#include "vsMacPermissions.h"
-#else
-#include "vsPermissions.h"
-#endif
-
-#ifndef NO_FEEDBACK
-#include "../Analyzer.h"
-#endif
-
-#include "../JackTrip.h"
-#include "../Meter.h"
-#include "../Monitor.h"
-#include "../Tone.h"
-#include "../Volume.h"
-#include "AudioInterfaceMode.h"
-
-// generic function to wait for a signal to be emitted
-template<typename SignalSenderPtr, typename SignalFuncPtr>
-static inline void WaitForSignal(SignalSenderPtr sender, SignalFuncPtr signal)
-{
- QTimer timer;
- timer.setTimerType(Qt::CoarseTimer);
- timer.setSingleShot(true);
-
- QEventLoop loop;
- QObject::connect(sender, signal, &loop, &QEventLoop::quit);
- QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
- timer.start(10000); // wait for max 10 seconds
- loop.exec();
-}
-
-// Constructor
-VsAudio::VsAudio(QObject* parent)
- : QObject(parent)
- , m_inputMeterLevels(2, 0)
- , m_outputMeterLevels(2, 0)
- , m_inputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
- , m_outputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
- , m_inputChannelsComboModel(
- QJsonArray::fromStringList(QStringList(QLatin1String(""))))
- , m_outputChannelsComboModel(
- QJsonArray::fromStringList(QStringList(QLatin1String(""))))
- , m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
- , m_audioWorkerPtr(new VsAudioWorker(this))
- , m_workerThreadPtr(nullptr)
- , m_inputMeterPluginPtr(nullptr)
- , m_outputMeterPluginPtr(nullptr)
- , m_inputVolumePluginPtr(nullptr)
- , m_outputVolumePluginPtr(nullptr)
- , m_monitorPluginPtr(nullptr)
- , mHasErrors(false)
-{
- loadSettings();
-
- QJsonObject element;
- element.insert(QString::fromStdString("label"), QString::fromStdString("Mono"));
- element.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MONO));
- m_inputMixModeComboModel = QJsonArray();
- m_inputMixModeComboModel.push_back(element);
-
- element = QJsonObject();
- element.insert(QString::fromStdString("label"), QString::fromStdString("1"));
- element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
- element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
- m_inputChannelsComboModel = QJsonArray();
- m_inputChannelsComboModel.push_back(element);
-
- element = QJsonObject();
- element.insert(QString::fromStdString("label"), QString::fromStdString("1 & 2"));
- element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
- element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt());
- m_outputChannelsComboModel = QJsonArray();
- m_outputChannelsComboModel.push_back(element);
-
- // Initialize timers needed for clip indicators
- m_inputClipTimer.setTimerType(Qt::CoarseTimer);
- m_inputClipTimer.setSingleShot(true);
- m_inputClipTimer.setInterval(3000);
- m_outputClipTimer.setTimerType(Qt::CoarseTimer);
- m_outputClipTimer.setSingleShot(true);
- m_outputClipTimer.setInterval(3000);
- m_inputClipTimer.callOnTimeout([&]() {
- if (m_inputClipped) {
- m_inputClipped = false;
- emit updatedInputClipped(m_inputClipped);
- }
- });
- m_outputClipTimer.callOnTimeout([&]() {
- if (m_outputClipped) {
- m_outputClipped = false;
- emit updatedOutputClipped(m_outputClipped);
- }
- });
-
- // move audio worker to its own thread
- 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(),
- &VsAudioWorker::openAudioInterface, Qt::QueuedConnection);
- connect(this, &VsAudio::signalStopAudio, m_audioWorkerPtr.get(),
- &VsAudioWorker::closeAudioInterface, Qt::QueuedConnection);
-#ifdef RT_AUDIO
- connect(this, &VsAudio::signalRefreshDevices, m_audioWorkerPtr.get(),
- &VsAudioWorker::refreshDevices, Qt::QueuedConnection);
- connect(this, &VsAudio::signalValidateDevices, m_audioWorkerPtr.get(),
- &VsAudioWorker::validateDevices, Qt::QueuedConnection);
- connect(m_audioWorkerPtr.get(), &VsAudioWorker::signalUpdatedDeviceModels, this,
- &VsAudio::setDeviceModels, Qt::QueuedConnection);
-#endif
-
- // Add permissions for Mac
-#ifdef __APPLE__
- m_permissionsPtr.reset(new VsMacPermissions());
- if (m_permissionsPtr->micPermissionChecked()
- && m_permissionsPtr->micPermission() == "unknown") {
- m_permissionsPtr->getMicPermission();
- }
-#else
- m_permissionsPtr.reset(new VsPermissions());
-#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<AudioInterfaceMode::JACK>()
- || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
- return true;
- } else {
- return false;
- }
-}
-
-bool VsAudio::jackIsAvailable() const
-{
- if constexpr (isBackendAvailable<AudioInterfaceMode::JACK>()) {
-#ifdef USE_WEAK_JACK
- // Check if Jack is available
- return (have_libjack() == 0);
-#else
- return true;
-#endif
- } else {
- return false;
- }
-}
-
-void VsAudio::setAudioReady(bool ready)
-{
- if (ready == m_audioReady)
- return;
- m_audioReady = ready;
- emit signalAudioReadyChanged();
- if (m_audioReady)
- emit signalAudioIsReady();
- else
- emit signalAudioIsNotReady();
-}
-
-void VsAudio::setScanningDevices(bool b)
-{
- if (b == m_scanningDevices)
- return;
- m_scanningDevices = b;
- emit signalScanningDevicesChanged();
-}
-
-void VsAudio::setAudioBackend(const QString& backend)
-{
- bool useRtAudio = (backend == QStringLiteral("RtAudio"));
- if (useRtAudio) {
- if (getUseRtAudio())
- return;
- m_backend = AudioBackendType::RTAUDIO;
- } else {
- if (!getUseRtAudio())
- return;
- m_backend = AudioBackendType::JACK;
- }
- emit audioBackendChanged(useRtAudio);
-}
-
-void VsAudio::setFeedbackDetectionEnabled(bool enabled)
-{
- if (m_feedbackDetectionEnabled == enabled)
- return;
- m_feedbackDetectionEnabled = enabled;
- emit feedbackDetectionEnabledChanged();
-}
-
-void VsAudio::setSampleRate(int sampleRate)
-{
- if (m_audioSampleRate == sampleRate)
- return;
- m_audioSampleRate = sampleRate;
- emit sampleRateChanged();
-}
-
-void VsAudio::setBufferSize(int bufSize)
-{
- if (m_audioBufferSize == bufSize)
- return;
- m_audioBufferSize = bufSize;
- emit bufferSizeChanged();
-}
-
-void VsAudio::setBufferStrategy(int bufStrategy)
-{
- if (m_bufferStrategy == bufStrategy)
- return;
- m_bufferStrategy = bufStrategy;
- emit bufferStrategyChanged();
-}
-
-void VsAudio::setNumInputChannels(int numChannels)
-{
- if (numChannels == m_numInputChannels)
- return;
- m_numInputChannels = numChannels;
- emit numInputChannelsChanged(numChannels);
-}
-
-void VsAudio::setNumOutputChannels(int numChannels)
-{
- if (numChannels == m_numOutputChannels)
- return;
- m_numOutputChannels = numChannels;
- emit numOutputChannelsChanged(numChannels);
-}
-
-void VsAudio::setBaseInputChannel(int baseChannel)
-{
- if (baseChannel == m_baseInputChannel)
- return;
- m_baseInputChannel = baseChannel;
- emit baseInputChannelChanged(baseChannel);
- return;
-}
-
-void VsAudio::setBaseOutputChannel(int baseChannel)
-{
- if (baseChannel == m_baseOutputChannel)
- return;
- m_baseOutputChannel = baseChannel;
- emit baseOutputChannelChanged(baseChannel);
- return;
-}
-
-void VsAudio::setInputMixMode(const int mode)
-{
- if (mode == m_inputMixMode)
- return;
- m_inputMixMode = mode;
- emit inputMixModeChanged(mode);
- return;
-}
-
-void VsAudio::setInputMuted(bool muted)
-{
- if (m_inMuted == muted)
- return;
- m_inMuted = muted;
- emit updatedInputMuted(muted);
-}
-
-void VsAudio::setInputVolume(float multiplier)
-{
- if (multiplier == m_inMultiplier)
- return;
- m_inMultiplier = multiplier;
- emit updatedInputVolume(multiplier);
-}
-
-void VsAudio::setOutputVolume(float multiplier)
-{
- if (multiplier == m_outMultiplier)
- return;
- m_outMultiplier = multiplier;
- emit updatedOutputVolume(multiplier);
-}
-
-void VsAudio::setMonitorVolume(float multiplier)
-{
- if (multiplier == m_monMultiplier)
- return;
- m_monMultiplier = multiplier;
- emit updatedMonitorVolume(multiplier);
-}
-
-void VsAudio::setInputDevice([[maybe_unused]] const QString& device)
-{
- if (!getUseRtAudio())
- return;
-#ifdef RT_AUDIO
- if (device == m_inputDevice)
- return;
- m_inputDevice = device;
- emit inputDeviceChanged(m_inputDevice);
-#endif
-}
-
-void VsAudio::setOutputDevice([[maybe_unused]] const QString& device)
-{
- if (!getUseRtAudio())
- return;
-#ifdef RT_AUDIO
- if (device == m_outputDevice)
- return;
- m_outputDevice = device;
- emit outputDeviceChanged(m_outputDevice);
-#endif
-}
-
-void VsAudio::setDevicesErrorMsg(const QString& msg)
-{
- if (m_devicesErrorMsg == msg)
- return;
- m_devicesErrorMsg = msg;
- emit devicesErrorChanged();
-}
-
-void VsAudio::setDevicesWarningMsg(const QString& msg)
-{
- if (m_devicesWarningMsg == msg)
- return;
- m_devicesWarningMsg = msg;
- emit devicesWarningChanged();
-}
-
-void VsAudio::setDevicesErrorHelpUrl(const QString& url)
-{
- if (m_devicesErrorHelpUrl == url)
- return;
- m_devicesErrorHelpUrl = url;
- emit devicesErrorHelpUrlChanged();
-}
-
-void VsAudio::setDevicesWarningHelpUrl(const QString& url)
-{
- if (m_devicesWarningHelpUrl == url)
- return;
- m_devicesWarningHelpUrl = url;
- emit devicesWarningHelpUrlChanged();
-}
-
-void VsAudio::setHighLatencyFlag(bool highLatencyFlag)
-{
- if (m_highLatencyFlag == highLatencyFlag)
- return;
- m_highLatencyFlag = highLatencyFlag;
- emit highLatencyFlagChanged(highLatencyFlag);
-}
-
-void VsAudio::startAudio(bool block)
-{
- // note this is also used for restartAudio()
- emit signalStartAudio();
- if (!block)
- return;
- WaitForSignal(this, &VsAudio::signalAudioIsReady);
-}
-
-void VsAudio::stopAudio(bool block)
-{
- if (!getAudioReady())
- return;
- emit signalStopAudio();
- if (!block)
- return;
- WaitForSignal(this, &VsAudio::signalAudioIsNotReady);
-}
-
-void VsAudio::refreshDevices(bool block)
-{
- if (!getUseRtAudio())
- return;
- emit signalRefreshDevices();
- if (!block)
- return;
- WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
-}
-
-void VsAudio::validateDevices(bool block)
-{
- if (!getUseRtAudio())
- return;
- emit signalValidateDevices();
- if (!block)
- return;
- WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
-}
-
-void VsAudio::loadSettings()
-{
- QSettings settings;
- settings.beginGroup(QStringLiteral("Audio"));
- setInputVolume(settings.value(QStringLiteral("InMultiplier"), 1).toFloat());
- setOutputVolume(settings.value(QStringLiteral("OutMultiplier"), 1).toFloat());
- setMonitorVolume(settings.value(QStringLiteral("MonMultiplier"), 0).toFloat());
- // note: we should always reset input muted to false; otherwise, bad things
- setInputMuted(false);
- // setInputMuted(settings.value(QStringLiteral("InMuted"), false).toBool());
-
- // load audio backend
- AudioBackendType audioBackend;
- if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()) {
- audioBackend =
- (settings.value(QStringLiteral("Backend"), AudioBackendType::RTAUDIO).toInt()
- == 1)
- ? AudioBackendType::RTAUDIO
- : AudioBackendType::JACK;
- } else if constexpr (isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
- audioBackend = AudioBackendType::RTAUDIO;
- } else {
- audioBackend = AudioBackendType::JACK;
- }
- if (audioBackend != m_backend) {
- setAudioBackend(audioBackend == AudioBackendType::RTAUDIO
- ? QStringLiteral("RtAudio")
- : QStringLiteral("JACK"));
- }
-
- // load input and output devices
- QString inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString();
- QString outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString();
- if (inputDevice == QStringLiteral("(default)")) {
- inputDevice = "";
- }
- if (outputDevice == QStringLiteral("(default)")) {
- outputDevice = "";
- }
- setInputDevice(inputDevice);
- setOutputDevice(outputDevice);
-
- // use default base channel 0, if the setting does not exist
- setBaseInputChannel(settings.value(QStringLiteral("BaseInputChannel"), 0).toInt());
- setBaseOutputChannel(settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt());
-
- // Handle migration scenarios. Assume this is a new user
- // if we have m_inputDevice == "" and m_outputDevice == ""
- if (m_inputDevice == "" && m_outputDevice == "") {
- // for fresh installs, use mono by default
- setNumInputChannels(
- settings.value(QStringLiteral("NumInputChannels"), 1).toInt());
- setInputMixMode(settings
- .value(QStringLiteral("InputMixMode"),
- static_cast<int>(AudioInterface::MONO))
- .toInt());
-
- // use 2 channels for output
- setNumOutputChannels(
- settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
- } else {
- // existing installs - keep using stereo
- setNumInputChannels(
- settings.value(QStringLiteral("NumInputChannels"), 2).toInt());
- setInputMixMode(settings
- .value(QStringLiteral("InputMixMode"),
- static_cast<int>(AudioInterface::STEREO))
- .toInt());
-
- // use 2 channels for output
- setNumOutputChannels(
- settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
- }
-
- setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt());
- int buffer_strategy = settings.value(QStringLiteral("BufferStrategy"), 2).toInt();
- if (buffer_strategy == 3 || buffer_strategy == 4)
- buffer_strategy = 2;
- setBufferStrategy(buffer_strategy);
- setFeedbackDetectionEnabled(
- settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool());
- settings.endGroup();
-}
-
-void VsAudio::saveSettings()
-{
- QSettings settings;
- settings.beginGroup(QStringLiteral("Audio"));
- settings.setValue(QStringLiteral("InMultiplier"), m_inMultiplier);
- settings.setValue(QStringLiteral("OutMultiplier"), m_outMultiplier);
- settings.setValue(QStringLiteral("MonMultiplier"), m_monMultiplier);
- // settings.setValue(QStringLiteral("InMuted"), m_inMuted);
- settings.setValue(QStringLiteral("Backend"), getUseRtAudio() ? 1 : 0);
- settings.setValue(QStringLiteral("InputDevice"), m_inputDevice);
- settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice);
- settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel);
- settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels);
- settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode);
- settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel);
- settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels);
- settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize);
- settings.setValue(QStringLiteral("BufferStrategy"), m_bufferStrategy);
- settings.setValue(QStringLiteral("FeedbackDetectionEnabled"),
- m_feedbackDetectionEnabled);
- settings.endGroup();
-}
-
-void VsAudio::detectedFeedbackLoop()
-{
- setInputMuted(true);
- setMonitorVolume(0);
- emit feedbackDetected();
-}
-
-void VsAudio::updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels)
-{
- 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 < numChannels) {
- dB = std::max(m_meterMin, valuesInDecibels[i]);
- }
-
- // Produce a normalized value from 0 to 1
- m_inputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
-
- // Signal a clip if we haven't done so already
- if (dB >= -0.05 && !detectedClip) {
- m_inputClipTimer.start();
- m_inputClipped = true;
- emit updatedInputClipped(m_inputClipped);
- detectedClip = true;
- }
- }
-
-#ifdef RT_AUDIO
- // For certain specific cases, copy the first channel's value into the second
- // channel's value
- if (getUseRtAudio()
- && ((m_inputMixMode == static_cast<int>(AudioInterface::MONO)
- && m_numInputChannels == 1)
- || (m_inputMixMode == static_cast<int>(AudioInterface::MIXTOMONO)
- && m_numInputChannels == 2))) {
- m_inputMeterLevels[1] = m_inputMeterLevels[0];
- }
-#endif
-
- emit updatedInputMeterLevels(m_inputMeterLevels);
-}
-
-void VsAudio::updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels)
-{
- 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 < numChannels) {
- dB = std::max(m_meterMin, valuesInDecibels[i]);
- }
-
- // Produce a normalized value from 0 to 1
- m_outputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
-
- // Signal a clip if we haven't done so already
- if (dB >= -0.05 && !detectedClip) {
- m_outputClipTimer.start();
- m_outputClipped = true;
- emit updatedOutputClipped(m_outputClipped);
- detectedClip = true;
- }
- }
-#ifdef RT_AUDIO
- if (m_numOutputChannels == 1) {
- m_outputMeterLevels[1] = m_outputMeterLevels[0];
- }
-#endif
- emit updatedOutputMeterLevels(m_outputMeterLevels);
-}
-
-void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
- int numInputChannels, int numOutputChannels)
-{
- // Make sure clip timers are stopped
- m_inputClipTimer.stop();
- m_outputClipTimer.stop();
-
- // Reset meters
- m_inputMeterLevels[0] = m_inputMeterLevels[1] = 0;
- m_outputMeterLevels[0] = m_outputMeterLevels[1] = 0;
- m_inputClipped = m_outputClipped = false;
- emit updatedInputMeterLevels(m_inputMeterLevels);
- emit updatedOutputMeterLevels(m_outputMeterLevels);
- emit updatedInputClipped(m_inputClipped);
- emit updatedOutputClipped(m_outputClipped);
- setInputMuted(false);
-
- // Create plugins
- m_inputMeterPluginPtr = new Meter(numInputChannels);
- m_outputMeterPluginPtr = new Meter(numOutputChannels);
- m_inputVolumePluginPtr = new Volume(numInputChannels);
- m_outputVolumePluginPtr = new Volume(numOutputChannels);
-
- // initialize input and output volumes
- m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
- m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
- m_inputVolumePluginPtr->muteUpdated(m_inMuted);
-
- // Connect plugins for communication with UI
- connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
- &VsAudio::updatedInputVuMeasurements);
- connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
- &VsAudio::updatedOutputVuMeasurements);
- connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr,
- &Volume::volumeUpdated);
- connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr,
- &Volume::volumeUpdated);
- connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr,
- &Volume::muteUpdated);
-
- // Note that plugin ownership is passed to the JackTrip class
- // In particular, the AudioInterface that it uses to connect
- audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr);
- audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr);
-
- if (forJackTrip) {
- // plugins for stream going to audio interface
- audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
-
- // Setup monitor
- // Note: Constructor determines how many internal monitor buffers to allocate
- m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels));
- m_monitorPluginPtr->volumeUpdated(m_monMultiplier);
- audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr);
- connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr,
- &Monitor::volumeUpdated);
-
-#ifndef NO_FEEDBACK
- // Setup output analyzer
- if (m_feedbackDetectionEnabled) {
- m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
- m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
- audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr);
- connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
- &VsAudio::detectedFeedbackLoop);
- }
-#endif
-
- // Setup output meter
- // Note: Add this to monitor process to include self-volume
- m_outputMeterPluginPtr->setIsMonitoringMeter(true);
- audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr);
-
- } else {
- // tone plugin is used to test audio output
- Tone* outputTonePluginPtr = new Tone(getNumOutputChannels());
- connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr,
- &Tone::triggerPlayback);
- audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr);
-
- // plugins for stream going to audio interface
- audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
- audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr);
- }
-}
-
-void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel)
-{
- m_inputComboModel = inputComboModel;
- m_outputComboModel = outputComboModel;
- emit inputComboModelChanged();
- emit outputComboModelChanged();
- if (!m_deviceModelsInitialized) {
- m_deviceModelsInitialized = true;
- emit deviceModelsInitializedChanged(true);
- }
-}
-
-void VsAudio::setInputChannelsComboModel(QJsonArray& model)
-{
- m_inputChannelsComboModel = model;
- emit inputChannelsComboModelChanged();
-}
-
-void VsAudio::setOutputChannelsComboModel(QJsonArray& model)
-{
- m_outputChannelsComboModel = model;
- emit outputChannelsComboModelChanged();
-}
-
-void VsAudio::setInputMixModeComboModel(QJsonArray& model)
-{
- m_inputMixModeComboModel = model;
- emit inputMixModeComboModelChanged();
-}
-
-void VsAudio::updateDeviceMessages(AudioInterface& audioInterface)
-{
- QString devicesWarningMsg =
- QString::fromStdString(audioInterface.getDevicesWarningMsg());
- QString devicesErrorMsg = QString::fromStdString(audioInterface.getDevicesErrorMsg());
- QString devicesWarningHelpUrl =
- QString::fromStdString(audioInterface.getDevicesWarningHelpUrl());
- QString devicesErrorHelpUrl =
- QString::fromStdString(audioInterface.getDevicesErrorHelpUrl());
-
- if (devicesWarningMsg != "") {
- qDebug() << "Devices Warning: " << devicesWarningMsg;
- if (devicesWarningHelpUrl != "") {
- qDebug() << "Learn More: " << devicesWarningHelpUrl;
- }
- }
-
- if (devicesErrorMsg != "") {
- qDebug() << "Devices Error: " << devicesErrorMsg;
- if (devicesErrorHelpUrl != "") {
- qDebug() << "Learn More: " << devicesErrorHelpUrl;
- }
- }
-
- setDevicesWarningMsg(devicesWarningMsg);
- setDevicesErrorMsg(devicesErrorMsg);
- setDevicesWarningHelpUrl(devicesWarningHelpUrl);
- setDevicesErrorHelpUrl(devicesErrorHelpUrl);
- setHighLatencyFlag(audioInterface.getHighLatencyFlag());
-}
-
-AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr)
-{
- // Create AudioInterface Client Object
- AudioInterface* ifPtr = nullptr;
- if (m_backend == VsAudio::AudioBackendType::JACK) {
- if (!isBackendAvailable<AudioInterfaceMode::JACK>()) {
- throw std::runtime_error(
- "JackTrip was not compiled with support for the Jack backend. "
- "In order to use Jack, you'll need to "
- "rebuild with Jack support.");
- }
- if (!jackIsAvailable()) {
- throw std::runtime_error(
- "Unable to load the Jack client library. "
- "In order to use Jack, you'll need to first install it.");
- }
- qDebug() << "Using JACK backend";
- ifPtr = newJackAudioInterface(jackTripPtr);
- } else if (m_backend == VsAudio::AudioBackendType::RTAUDIO) {
- if (!isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
- throw std::runtime_error(
- "JackTrip was not compiled with support for the RtAudio backend. "
- "In order to use RtAudio, you'll need to "
- "rebuild with RtAudio support.");
- }
- qDebug() << "Using RtAudio backend";
- ifPtr = newRtAudioInterface(jackTripPtr);
- } else {
- throw std::runtime_error("Unknown audio backend");
- }
-
- mHasErrors = false;
- ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) {
- this->errorCallback(errorText, jackTripPtr);
- });
-
- // AudioInterface::setup() can return a different buffer size
- // if the audio interface doesn't support the one that was requested
- if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) {
- setBufferSize(ifPtr->getBufferSizeInSamples());
- }
-
- std::cout << "The Sampling Rate is: " << ifPtr->getSampleRate() << std::endl;
- std::cout << gPrintSeparator << std::endl;
- int AudioBufferSizeInBytes = ifPtr->getBufferSizeInSamples() * sizeof(sample_t);
- std::cout << "The Audio Buffer Size is: " << ifPtr->getBufferSizeInSamples()
- << " samples" << std::endl;
- std::cout << " or: " << AudioBufferSizeInBytes << " bytes"
- << std::endl;
- std::cout << gPrintSeparator << std::endl;
- std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels()
- << std::endl;
- std::cout << gPrintSeparator << std::endl;
-
- // setup audio plugins
- appendProcessPlugins(*ifPtr, jackTripPtr != nullptr, getNumInputChannels(),
- getNumOutputChannels());
-
- return ifPtr;
-}
-
-AudioInterface* VsAudio::newJackAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
-{
- AudioInterface* ifPtr = nullptr;
-#ifndef NO_JACK
- static const int numJackChannels = 2;
- if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()
- || isBackendAvailable<AudioInterfaceMode::JACK>()) {
- QVarLengthArray<int> inputChans;
- QVarLengthArray<int> outputChans;
- inputChans.resize(numJackChannels);
- outputChans.resize(numJackChannels);
-
- for (int i = 0; i < numJackChannels; i++) {
- inputChans[i] = 1 + i;
- }
- for (int i = 0; i < numJackChannels; i++) {
- outputChans[i] = 1 + i;
- }
-
- ifPtr = new JackAudioInterface(inputChans, outputChans, m_audioBitResolution,
- jackTripPtr != nullptr, jackTripPtr);
- ifPtr->setClientName(QStringLiteral("JackTrip"));
-#if defined(__unix__)
- AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate());
-#endif
- ifPtr->setup(true);
- }
-#endif
- return ifPtr;
-}
-
-AudioInterface* VsAudio::newRtAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
-{
- AudioInterface* ifPtr = nullptr;
-#ifdef RT_AUDIO
- QVarLengthArray<int> inputChans;
- QVarLengthArray<int> outputChans;
- inputChans.resize(getNumInputChannels());
- outputChans.resize(getNumOutputChannels());
-
- for (int i = 0; i < getNumInputChannels(); i++) {
- inputChans[i] = getBaseInputChannel() + i;
- }
- for (int i = 0; i < getNumOutputChannels(); i++) {
- outputChans[i] = getBaseOutputChannel() + i;
- }
-
- ifPtr = new RtAudioInterface(
- inputChans, outputChans,
- static_cast<AudioInterface::inputMixModeT>(getInputMixMode()),
- m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr);
- ifPtr->setSampleRate(getSampleRate());
- ifPtr->setInputDevice(getInputDevice().toStdString());
- ifPtr->setOutputDevice(getOutputDevice().toStdString());
- ifPtr->setBufferSizeInSamples(getBufferSize());
-
- QVector<RtAudioDevice> devices = m_audioWorkerPtr->getDevices();
- if (!devices.empty())
- static_cast<RtAudioInterface*>(ifPtr)->setRtAudioDevices(devices);
-
-#if defined(__unix__)
- AudioInterface::setPipewireLatency(getBufferSize(), ifPtr->getSampleRate());
-#endif
-
- // Note: setup might change the number of channels and/or buffer size
- ifPtr->setup(true);
-
- // TODO: Add check for if base input channel needs to change
- if (jackTripPtr != nullptr && getNumInputChannels() == 2
- && getInputMixMode() == AudioInterface::MIXTOMONO)
- jackTripPtr->setNumInputChannels(1);
-
-#endif // RT_AUDIO
- return ifPtr;
-}
-
-void VsAudio::errorCallback(const std::string& errorText,
- [[maybe_unused]] JackTrip* jackTripPtr)
-{
- const QString errorMsg(QString::fromStdString(errorText));
- setDevicesErrorMsg(errorMsg);
-#ifdef _WIN32
- // handle special case for Windows ASIO drivers that trigger
- // asynchronous errors shortly after you try opening the
- // RtAudio stream with a different sample rate (only for audio tester)
- if (jackTripPtr == nullptr && getUseRtAudio()
- && errorMsg.contains("sample rate changed")) {
- // only refresh devices once
- if (mHasErrors)
- return;
- mHasErrors = true;
- // asynchronously refresh devices
- refreshDevices(false);
- }
-#else
- mHasErrors = true;
-#endif
-}
-
-// VsAudioWorker methods
-
-VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {}
-
-void VsAudioWorker::openAudioInterface()
-{
-#ifdef __APPLE__
- if (m_parentPtr->m_permissionsPtr->micPermission() != "granted") {
- return;
- }
-#endif
-
- if constexpr (!(isBackendAvailable<AudioInterfaceMode::JACK>()
- || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
- return;
- }
-
- if (!m_audioInterfacePtr.isNull()) {
- std::cout << "Restarting Audio" << std::endl;
- closeAudioInterface();
- } else {
- std::cout << "Starting Audio" << std::endl;
- }
-
- unsigned int maxTries = 2;
-#ifdef RT_AUDIO
- // Update devices, if not already initialized
- if (getUseRtAudio()) {
- if (!m_parentPtr->getDeviceModelsInitialized()) {
- updateDeviceModels();
- maxTries = 1;
- }
- }
-#endif
- for (unsigned int tryNum = 0; tryNum < maxTries; ++tryNum) {
-#ifdef RT_AUDIO
- if (tryNum > 0) {
- if (getUseRtAudio()) {
- updateDeviceModels();
- } else {
- m_parentPtr->setAudioBackend("RtAudio");
- updateDeviceModels();
- }
- }
-#endif
- try {
- // create and setup a new audio interface
- m_audioInterfacePtr.reset(m_parentPtr->newAudioInterface());
- // success if it doesn't throw
- break;
- } catch (const std::exception& e) {
- emit signalError(QString::fromUtf8(e.what()));
- }
- }
-
- if (m_audioInterfacePtr.isNull()) {
- return;
- }
-
- // initialize plugins and start the audio callback process
- m_audioInterfacePtr->initPlugins(false);
- m_audioInterfacePtr->startProcess();
- m_audioInterfacePtr->connectDefaultPorts();
-
- m_parentPtr->updateDeviceMessages(*m_audioInterfacePtr);
- m_parentPtr->setAudioReady(true);
-}
-
-void VsAudioWorker::closeAudioInterface()
-{
- if (m_audioInterfacePtr.isNull())
- return;
- std::cout << "Stopping Audio" << std::endl;
- try {
- m_audioInterfacePtr->stopProcess();
- } catch (const std::exception& e) {
- emit signalError(QString::fromUtf8(e.what()));
- }
- m_audioInterfacePtr.clear();
- m_parentPtr->setAudioReady(false);
-}
-
-#ifdef RT_AUDIO
-
-void VsAudioWorker::refreshDevices()
-{
- if (!getUseRtAudio())
- return;
- bool restartAudio = !m_audioInterfacePtr.isNull();
- if (restartAudio)
- closeAudioInterface();
- updateDeviceModels();
- if (restartAudio)
- openAudioInterface();
-}
-
-void VsAudioWorker::updateDeviceModels()
-{
- if (!getUseRtAudio())
- return;
-
- // note: audio must not be active when scanning devices
- m_parentPtr->setScanningDevices(true);
- closeAudioInterface();
- RtAudioInterface::scanDevices(m_devices);
-
- QStringList inputDeviceCategories;
- QStringList outputDeviceCategories;
-
- getDeviceList(m_devices, m_inputDeviceList, inputDeviceCategories,
- m_inputDeviceChannels, true);
- getDeviceList(m_devices, m_outputDeviceList, outputDeviceCategories,
- m_outputDeviceChannels, false);
-
- QJsonArray inputComboModel =
- formatDeviceList(m_inputDeviceList, inputDeviceCategories, m_inputDeviceChannels);
- QJsonArray outputComboModel = formatDeviceList(
- m_outputDeviceList, outputDeviceCategories, m_outputDeviceChannels);
-
- validateDevices();
-
- // let VsAudio know that things have been updated
- m_parentPtr->setScanningDevices(false);
- emit signalUpdatedDeviceModels(inputComboModel, outputComboModel);
-}
-
-void VsAudioWorker::getDeviceList(const QVector<RtAudioDevice>& devices,
- QStringList& list, QStringList& categories,
- QList<int>& channels, bool isInput)
-{
- categories.clear();
- channels.clear();
- list.clear();
-
- // do not include blacklisted audio interfaces
- // these are known to be unstable and cause JackTrip to crash
- QVector<QString> blacklisted_devices = {
-#ifdef _WIN32
- // Realtek ASIO: seems to crash any computer that tries to use it
- QString::fromUtf8("Realtek ASIO"),
- QString::fromUtf8("Generic Low Latency ASIO Driver"),
-#endif
- // JackRouter: crashes if not running; use Jack backend instead
- QString::fromUtf8("JackRouter"),
- };
-
- for (int n = 0; n < devices.size(); ++n) {
-#ifdef _WIN32
- if (devices[n].api == RtAudio::UNIX_JACK) {
- continue;
- }
-#endif
- const QString deviceName(QString::fromStdString(devices[n].name));
-
- // Don't include duplicate entries
- if (list.contains(deviceName)) {
- continue;
- }
-
- // Skip if no channels available
- if ((isInput && devices[n].inputChannels == 0)
- || (!isInput && devices[n].outputChannels == 0)) {
- continue;
- }
-
- // Skip blacklisted devices
- if (blacklisted_devices.contains(deviceName)) {
- std::cout << "RTAudio: blacklisted " << (isInput ? "input" : "output")
- << " device: " << devices[n].name << std::endl;
- continue;
- }
-
- // Good to go!
- if (isInput) {
- list.append(deviceName);
- channels.append(devices[n].inputChannels);
- } else {
- list.append(deviceName);
- channels.append(devices[n].outputChannels);
- }
-
- switch (devices[n].api) {
- case RtAudio::WINDOWS_ASIO:
- categories.append("Low-Latency (ASIO)");
- break;
- case RtAudio::WINDOWS_WASAPI:
- categories.append("High-Latency (WASAPI)");
- break;
- case RtAudio::WINDOWS_DS:
- categories.append("High-Latency (DirectSound)");
- break;
- case RtAudio::LINUX_ALSA:
- categories.append("Low-Latency (ALSA)");
- break;
- case RtAudio::LINUX_PULSE:
- categories.append("High-Latency (Pulse)");
- break;
- case RtAudio::LINUX_OSS:
- categories.append("High-Latency (OSS)");
- break;
- default:
- categories.append("");
- break;
- }
- }
-}
-
-QJsonArray VsAudioWorker::formatDeviceList(const QStringList& devices,
- const QStringList& categories,
- const QList<int>& channels)
-{
- QStringList uniqueCategories = QStringList(categories);
- uniqueCategories.removeDuplicates();
-
- bool containsCategories = true;
- if (uniqueCategories.size() == 0) {
- containsCategories = false;
- } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") {
- containsCategories = false;
- }
-
- QJsonArray items;
- for (int i = 0; i < uniqueCategories.size(); i++) {
- QString category = uniqueCategories.at(i);
-
- if (containsCategories) {
- QJsonObject header = QJsonObject();
- header.insert(QString::fromStdString("text"), category);
- header.insert(QString::fromStdString("type"),
- QString::fromStdString("header"));
- header.insert(QString::fromStdString("category"), category);
- items.push_back(header);
- }
-
- for (int j = 0; j < devices.size(); j++) {
- if (categories.at(j).toStdString() == category.toStdString()) {
- QJsonObject element = QJsonObject();
- element.insert(QString::fromStdString("text"), devices.at(j));
- element.insert(QString::fromStdString("type"),
- QString::fromStdString("element"));
- element.insert(QString::fromStdString("channels"), channels.at(j));
- element.insert(QString::fromStdString("category"), category);
- items.push_back(element);
- }
- }
- }
-
- return items;
-}
-
-void VsAudioWorker::validateDevices()
-{
- if (!getUseRtAudio())
- return;
- validateInputDevicesState();
- validateOutputDevicesState();
- emit signalDevicesValidated();
-}
-
-void VsAudioWorker::validateInputDevicesState()
-{
- if (!getUseRtAudio()) {
- return;
- }
- if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
- return;
- }
-
- // Given input device list, check that the currently set device
- // actually exists
- if (getInputDevice() == QStringLiteral("")
- || m_inputDeviceList.indexOf(getInputDevice()) == -1) {
- m_parentPtr->setInputDevice(m_inputDeviceList[0]);
- }
-
- // Given the currently selected input device, reset the available input channel
- // options
- int indexOfInput = m_inputDeviceList.indexOf(getInputDevice());
- if (indexOfInput == -1) {
- std::cerr << "Invalid state. Input device index should never be -1" << std::endl;
- return;
- }
-
- int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput);
- if (numDevicesChannelsAvailable < 1) {
- std::cerr << "Invalid state. Number of channels should never be less than 1"
- << std::endl;
- return;
- } else if (numDevicesChannelsAvailable == 1) {
- // Set the input mix mode to just have "Mono" as the option
- QJsonObject inputMixModeComboElement = QJsonObject();
- inputMixModeComboElement.insert(QString::fromStdString("label"),
- QString::fromStdString("Mono"));
- inputMixModeComboElement.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MONO));
- QJsonArray inputMixModeComboModel;
- inputMixModeComboModel.push_back(inputMixModeComboElement);
- m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
-
- // Set the input channels combo to only have channel 1 as an option
- QJsonObject inputChannelsComboElement;
- inputChannelsComboElement.insert(QString::fromStdString("label"),
- QString::fromStdString("1"));
- inputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
- QVariant(0).toInt());
- inputChannelsComboElement.insert(QString::fromStdString("numChannels"),
- QVariant(1).toInt());
- QJsonArray inputChannelsComboModel;
- inputChannelsComboModel.push_back(inputChannelsComboElement);
- m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
-
- // Set the only allowed options for these variables automatically
- m_parentPtr->setBaseInputChannel(0);
- m_parentPtr->setNumInputChannels(1);
- m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
- } else {
- // set the input channels selector to have the options based on the currently
- // selected device
- QJsonArray inputChannelsComboModel;
- for (int i = 0; i < numDevicesChannelsAvailable; i++) {
- QJsonObject element = QJsonObject();
- element.insert(QString::fromStdString("label"), QVariant(i + 1).toString());
- element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt());
- element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
- inputChannelsComboModel.push_back(element);
- }
- for (int i = 0; i < numDevicesChannelsAvailable; i++) {
- if (i % 2 == 0) {
- QJsonObject element = QJsonObject();
- element.insert(
- QString::fromStdString("label"),
- QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
- element.insert(QString::fromStdString("baseChannel"),
- QVariant(i).toInt());
- element.insert(QString::fromStdString("numChannels"),
- QVariant(2).toInt());
- inputChannelsComboModel.push_back(element);
- }
- }
- m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
-
- // if the current m_baseInputChannel or m_numInputChannels is invalid based on
- // this device's option, use the first two channels by default
- if (getBaseInputChannel() + getNumInputChannels() > numDevicesChannelsAvailable) {
- // we're in the case where numDevicesChannelsAvailable >= 2, so always have
- // the ability to use the first 2 channels
- m_parentPtr->setBaseInputChannel(0);
- m_parentPtr->setNumInputChannels(2);
- }
- if (getNumInputChannels() != 1) {
- // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if
- // we're using 2 channels
- QJsonObject inputMixModeComboElement1 = QJsonObject();
- inputMixModeComboElement1.insert(QString::fromStdString("label"),
- QString::fromStdString("Stereo"));
- inputMixModeComboElement1.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::STEREO));
- QJsonObject inputMixModeComboElement2 = QJsonObject();
- inputMixModeComboElement2.insert(QString::fromStdString("label"),
- QString::fromStdString("Mix to Mono"));
- inputMixModeComboElement2.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MIXTOMONO));
- QJsonArray inputMixModeComboModel;
- inputMixModeComboModel.push_back(inputMixModeComboElement1);
- inputMixModeComboModel.push_back(inputMixModeComboElement2);
- m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
-
- // if m_inputMixMode is an invalid value, set it to "stereo" by default
- // given that we are using 2 channels
- if (getInputMixMode() != static_cast<int>(AudioInterface::STEREO)
- && getInputMixMode() != static_cast<int>(AudioInterface::MIXTOMONO)) {
- m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::STEREO));
- }
- } else {
- // Set the input mix mode to just have "Mono" as the option if we're using 1
- // channel
- QJsonObject inputMixModeComboElement = QJsonObject();
- inputMixModeComboElement.insert(QString::fromStdString("label"),
- QString::fromStdString("Mono"));
- inputMixModeComboElement.insert(QString::fromStdString("value"),
- static_cast<int>(AudioInterface::MONO));
- QJsonArray inputMixModeComboModel;
- inputMixModeComboModel.push_back(inputMixModeComboElement);
- m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
-
- // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO
- if (getInputMixMode() != static_cast<int>(AudioInterface::MONO)) {
- m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
- }
- }
- }
-}
-
-void VsAudioWorker::validateOutputDevicesState()
-{
- if (!getUseRtAudio()) {
- return;
- }
- if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
- return;
- }
-
- // Given output device list, check that the currently set device
- // actually exists
- if (getOutputDevice() == QStringLiteral("")
- || m_outputDeviceList.indexOf(getOutputDevice()) == -1) {
- m_parentPtr->setOutputDevice(m_outputDeviceList[0]);
- }
-
- // Given the currently selected output device, reset the available output channel
- // options
- int indexOfOutput = m_outputDeviceList.indexOf(getOutputDevice());
- if (indexOfOutput == -1) {
- std::cerr << "Invalid state. Output device index should never be -1" << std::endl;
- return;
- }
-
- int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput);
- if (numDevicesChannelsAvailable < 1) {
- std::cerr << "Invalid state. Number of channels should never be less than 1"
- << std::endl;
- return;
- } else if (numDevicesChannelsAvailable == 1) {
- // Set the output channels combo to only have channel 1 as an option
- QJsonObject outputChannelsComboElement = QJsonObject();
- outputChannelsComboElement.insert(QString::fromStdString("label"),
- QString::fromStdString("1"));
- outputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
- QVariant(0).toInt());
- outputChannelsComboElement.insert(QString::fromStdString("numChannels"),
- QVariant(1).toInt());
- QJsonArray outputChannelsComboModel;
- outputChannelsComboModel.push_back(outputChannelsComboElement);
- m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
-
- // Set the only allowed options for these variables automatically
- m_parentPtr->setBaseOutputChannel(0);
- m_parentPtr->setNumOutputChannels(1);
- } else {
- // set the output channels selector to have the options based on the currently
- // selected device
- QJsonArray outputChannelsComboModel;
- for (int i = 0; i < numDevicesChannelsAvailable; i++) {
- if (i % 2 == 0) {
- QJsonObject element = QJsonObject();
- element.insert(
- QString::fromStdString("label"),
- QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
- element.insert(QString::fromStdString("baseChannel"),
- QVariant(i).toInt());
- element.insert(QString::fromStdString("numChannels"),
- QVariant(2).toInt());
- outputChannelsComboModel.push_back(element);
- }
- }
- m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
-
- // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on
- // this device's option, use the first two channels by default
- if (getBaseOutputChannel() + getNumOutputChannels()
- > numDevicesChannelsAvailable) {
- // we're in the case where numDevicesChannelsAvailable >= 2, so always have
- // the ability to use the first 2 channels
- m_parentPtr->setBaseOutputChannel(0);
- m_parentPtr->setNumOutputChannels(2);
- }
- }
-}
-
-#endif // RT_AUDIO
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsAudio.h
- * \author Matt Horton
- * \date September 2022
- */
-
-#ifndef VSDAUDIO_H
-#define VSDAUDIO_H
-
-#include <QJsonArray>
-#include <QList>
-#include <QObject>
-#include <QSharedPointer>
-#include <QString>
-#include <QStringList>
-#include <QTimer>
-
-#include "../AudioInterface.h"
-#include "../jacktrip_globals.h"
-
-#ifdef RT_AUDIO
-#include "../RtAudioInterface.h"
-#endif
-
-class Analyzer;
-class JackTrip;
-class Meter;
-class Monitor;
-class QThread;
-class Tone;
-class Volume;
-class VsPermissions;
-class VsAudioWorker;
-
-class VsAudio : public QObject
-{
- Q_OBJECT
-
- // state shared with QML
- Q_PROPERTY(bool audioReady READ getAudioReady NOTIFY signalAudioReadyChanged)
- Q_PROPERTY(bool scanningDevices READ getScanningDevices WRITE setScanningDevices
- NOTIFY signalScanningDevicesChanged)
- Q_PROPERTY(bool feedbackDetectionEnabled READ getFeedbackDetectionEnabled WRITE
- setFeedbackDetectionEnabled NOTIFY feedbackDetectionEnabledChanged)
- Q_PROPERTY(bool deviceModelsInitialized READ getDeviceModelsInitialized NOTIFY
- deviceModelsInitializedChanged)
- Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT)
- Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY
- audioBackendChanged)
- Q_PROPERTY(
- int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
- Q_PROPERTY(
- int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
- Q_PROPERTY(int bufferStrategy READ getBufferStrategy WRITE setBufferStrategy NOTIFY
- bufferStrategyChanged)
- Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels
- NOTIFY numInputChannelsChanged)
- Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels
- NOTIFY numOutputChannelsChanged)
- Q_PROPERTY(int baseInputChannel READ getBaseInputChannel WRITE setBaseInputChannel
- NOTIFY baseInputChannelChanged)
- Q_PROPERTY(int baseOutputChannel READ getBaseOutputChannel WRITE setBaseOutputChannel
- NOTIFY baseOutputChannelChanged)
- Q_PROPERTY(int inputMixMode READ getInputMixMode WRITE setInputMixMode NOTIFY
- inputMixModeChanged)
- Q_PROPERTY(
- bool inputMuted READ getInputMuted WRITE setInputMuted NOTIFY updatedInputMuted)
- Q_PROPERTY(bool inputClipped READ getInputClipped NOTIFY updatedInputClipped)
- Q_PROPERTY(bool outputClipped READ getOutputClipped NOTIFY updatedOutputClipped)
- Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY
- updatedInputVolume)
- Q_PROPERTY(float outputVolume READ getOutputVolume WRITE setOutputVolume NOTIFY
- updatedOutputVolume)
- Q_PROPERTY(float monitorVolume READ getMonitorVolume WRITE setMonitorVolume NOTIFY
- updatedMonitorVolume)
- Q_PROPERTY(QString inputDevice READ getInputDevice WRITE setInputDevice NOTIFY
- inputDeviceChanged)
- Q_PROPERTY(QString outputDevice READ getOutputDevice WRITE setOutputDevice NOTIFY
- outputDeviceChanged)
- Q_PROPERTY(QVector<float> inputMeterLevels READ getInputMeterLevels NOTIFY
- updatedInputMeterLevels)
- Q_PROPERTY(QVector<float> outputMeterLevels READ getOutputMeterLevels NOTIFY
- updatedOutputMeterLevels)
- Q_PROPERTY(
- QJsonArray inputComboModel READ getInputComboModel NOTIFY inputComboModelChanged)
- Q_PROPERTY(QJsonArray outputComboModel READ getOutputComboModel NOTIFY
- outputComboModelChanged)
- Q_PROPERTY(QJsonArray inputChannelsComboModel READ getInputChannelsComboModel NOTIFY
- inputChannelsComboModelChanged)
- Q_PROPERTY(QJsonArray outputChannelsComboModel READ getOutputChannelsComboModel NOTIFY
- outputChannelsComboModelChanged)
- Q_PROPERTY(QJsonArray inputMixModeComboModel READ getInputMixModeComboModel NOTIFY
- inputMixModeComboModelChanged)
- Q_PROPERTY(QStringList feedbackDetectionComboModel READ getFeedbackDetectionComboModel
- CONSTANT)
- Q_PROPERTY(QStringList bufferSizeComboModel READ getBufferSizeComboModel CONSTANT)
- Q_PROPERTY(
- QStringList bufferStrategyComboModel READ getBufferStrategyComboModel CONSTANT)
- Q_PROPERTY(QStringList audioBackendComboModel READ getAudioBackendComboModel CONSTANT)
- Q_PROPERTY(
- QString devicesWarning READ getDevicesWarningMsg NOTIFY devicesWarningChanged)
- Q_PROPERTY(QString devicesError READ getDevicesErrorMsg NOTIFY devicesErrorChanged)
- Q_PROPERTY(QString devicesWarningHelpUrl READ getDevicesWarningHelpUrl NOTIFY
- devicesWarningHelpUrlChanged)
- Q_PROPERTY(QString devicesErrorHelpUrl READ getDevicesErrorHelpUrl NOTIFY
- devicesErrorHelpUrlChanged)
- Q_PROPERTY(bool highLatencyFlag READ getHighLatencyFlag NOTIFY highLatencyFlagChanged)
-
- public:
- enum AudioBackendType {
- JACK = 0, ///< Jack Mode
- RTAUDIO ///< RtAudio Mode
- };
-
- // Constructor
- explicit VsAudio(QObject* parent = nullptr);
- virtual ~VsAudio();
-
- // allow VirtualStudio to get Permissions to bind to QML view
- VsPermissions& getPermissions() { return *m_permissionsPtr; }
- VsAudioWorker& getWorker() { return *m_audioWorkerPtr; }
-
- // allow VirtualStudio to create new audio interfaces
- AudioInterface* newAudioInterface(JackTrip* jackTripPtr = nullptr);
-
- // allow VirtualStudio to load and save settings
- void loadSettings();
- void saveSettings();
-
- // getters for state shared with QML
- bool backendAvailable() const;
- bool jackIsAvailable() const;
- bool getAudioReady() const { return m_audioReady; }
- bool getScanningDevices() const { return m_scanningDevices; }
- bool getFeedbackDetectionEnabled() const { return m_feedbackDetectionEnabled; }
- bool getDeviceModelsInitialized() const { return m_deviceModelsInitialized; }
- bool getUseRtAudio() const { return m_backend == AudioBackendType::RTAUDIO; }
- QString getAudioBackend() const
- {
- return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK");
- }
- int getSampleRate() const { return m_audioSampleRate; }
- int getBufferSize() const { return m_audioBufferSize; }
- int getBufferStrategy() const { return m_bufferStrategy; }
- int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; }
- int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; }
- int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; }
- int getBaseOutputChannel() const { return getUseRtAudio() ? m_baseOutputChannel : 0; }
- int getInputMixMode() const { return getUseRtAudio() ? m_inputMixMode : 0; }
- bool getInputMuted() const { return m_inMuted; }
- bool getInputClipped() const { return m_inputClipped; }
- bool getOutputClipped() const { return m_outputClipped; }
- float getInputVolume() const { return m_inMultiplier; }
- float getOutputVolume() const { return m_outMultiplier; }
- float getMonitorVolume() const { return m_monMultiplier; }
- const QString& getInputDevice() const { return m_inputDevice; }
- const QString& getOutputDevice() const { return m_outputDevice; }
- const QVector<float>& getInputMeterLevels() const { return m_inputMeterLevels; }
- const QVector<float>& getOutputMeterLevels() const { return m_outputMeterLevels; }
- const QJsonArray& getInputComboModel() const { return m_inputComboModel; }
- const QJsonArray& getOutputComboModel() const { return m_outputComboModel; }
- const QJsonArray& getInputChannelsComboModel() const
- {
- return m_inputChannelsComboModel;
- }
- const QJsonArray& getOutputChannelsComboModel() const
- {
- return m_outputChannelsComboModel;
- }
- const QJsonArray& getInputMixModeComboModel() const
- {
- return m_inputMixModeComboModel;
- }
- const QStringList& getFeedbackDetectionComboModel() const
- {
- return m_feedbackDetectionComboModel;
- }
- const QStringList& getBufferSizeComboModel() const { return m_bufferSizeComboModel; }
- const QStringList& getBufferStrategyComboModel() const
- {
- return m_bufferStrategyComboModel;
- }
- const QStringList& getAudioBackendComboModel() const
- {
- return m_audioBackendComboModel;
- }
- const QString& getDevicesWarningMsg() const { return m_devicesWarningMsg; }
- const QString& getDevicesErrorMsg() const { return m_devicesErrorMsg; }
- const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; }
- const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; }
- bool getHighLatencyFlag() const { return m_highLatencyFlag; }
- public slots:
-
- // setters for state shared with QML
- void setFeedbackDetectionEnabled(bool enabled);
- void setAudioBackend(const QString& backend);
- void setSampleRate(int sampleRate);
- void setBufferSize(int bufSize);
- void setBufferStrategy(int bufStrategy);
- void setNumInputChannels(int numChannels);
- void setNumOutputChannels(int numChannels);
- void setBaseInputChannel(int baseChannel);
- void setBaseOutputChannel(int baseChannel);
- void setInputMixMode(int mode);
- void setInputMuted(bool muted);
- void setInputVolume(float multiplier);
- void setOutputVolume(float multiplier);
- void setMonitorVolume(float multiplier);
- void setInputDevice(const QString& device);
- void setOutputDevice(const QString& device);
- void setDevicesErrorMsg(const QString& msg);
- void setDevicesWarningMsg(const QString& msg);
- void setDevicesErrorHelpUrl(const QString& url);
- void setDevicesWarningHelpUrl(const QString& url);
- void setHighLatencyFlag(bool highLatency);
-
- // public methods accessible by QML
- void startAudio(bool block = false);
- void stopAudio(bool block = false);
- void refreshDevices(bool block = false);
- void validateDevices(bool block = false);
- void restartAudio(bool block = false) { return startAudio(block); }
- void playOutputAudio() { emit signalPlayOutputAudio(); }
-
- signals:
-
- // signals for QML state changes
- void signalAudioReadyChanged();
- void signalAudioIsReady();
- void signalAudioIsNotReady();
- void signalScanningDevicesChanged();
- void deviceModelsInitializedChanged(bool initialized);
- void audioBackendChanged(bool useRtAudio);
- void sampleRateChanged();
- void bufferSizeChanged();
- void bufferStrategyChanged();
- void numInputChannelsChanged(int numChannels);
- void numOutputChannelsChanged(int numChannels);
- void baseInputChannelChanged(int baseChannel);
- void baseOutputChannelChanged(int baseChannel);
- void inputMixModeChanged(int mode);
- void updatedInputMuted(bool muted);
- void updatedInputClipped(bool clip);
- void updatedOutputClipped(bool clip);
- void updatedInputVolume(float multiplier);
- void updatedOutputVolume(float multiplier);
- void updatedMonitorVolume(float multiplier);
- void inputDeviceChanged(QString device);
- void outputDeviceChanged(QString device);
- void updatedInputMeterLevels(const QVector<float>& levels);
- void updatedOutputMeterLevels(const QVector<float>& levels);
- void feedbackDetectionEnabledChanged();
- void feedbackDetected();
- void inputComboModelChanged();
- void outputComboModelChanged();
- void inputChannelsComboModelChanged();
- void outputChannelsComboModelChanged();
- void inputMixModeComboModelChanged();
- void devicesWarningChanged();
- void devicesErrorChanged();
- void devicesWarningHelpUrlChanged();
- void devicesErrorHelpUrlChanged();
- void highLatencyFlagChanged(bool highLatencyFlag);
-
- // other signals to perform actions
- void signalPlayOutputAudio();
- void signalStartAudio();
- void signalStopAudio();
- void signalRefreshDevices();
- void signalValidateDevices();
- void signalDevicesValidated();
-
- private slots:
- void setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel);
- void setInputChannelsComboModel(QJsonArray& model);
- void setOutputChannelsComboModel(QJsonArray& model);
- void setInputMixModeComboModel(QJsonArray& model);
-
- private:
- // private methods
- void setAudioReady(bool ready);
- void setScanningDevices(bool b);
- void detectedFeedbackLoop();
- void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels);
- void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels);
- void appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
- int numInputChannels, int numOutputChannels);
- void updateDeviceMessages(AudioInterface& audioInterface);
- AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr);
- AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr);
- void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr);
-
- // range for volume meters
- static constexpr float m_meterMax = 0.0;
- static constexpr float m_meterMin = -64.0;
-
- // audio bit resolution
- static constexpr AudioInterface::audioBitResolutionT m_audioBitResolution =
- AudioInterface::BIT16;
-
- // state shared with QML
- AudioBackendType m_backend = AudioBackendType::JACK;
- bool m_audioReady = false;
- bool m_scanningDevices = false;
- bool m_feedbackDetectionEnabled = true;
- bool m_deviceModelsInitialized = false;
- int m_audioSampleRate = gDefaultSampleRate;
- int m_audioBufferSize =
- gDefaultBufferSizeInSamples; ///< Audio buffer size to process on each callback
- int m_bufferStrategy = 0;
- int m_numInputChannels = gDefaultNumInChannels;
- int m_numOutputChannels = gDefaultNumOutChannels;
- int m_baseInputChannel = 0;
- int m_baseOutputChannel = 0;
- int m_inputMixMode = 0;
- bool m_inMuted = false;
- bool m_inputClipped = false;
- bool m_outputClipped = false;
- float m_inMultiplier = 1.0;
- float m_outMultiplier = 1.0;
- float m_monMultiplier = 0;
-
- QString m_inputDevice;
- QString m_outputDevice;
- QVector<float> m_inputMeterLevels;
- QVector<float> m_outputMeterLevels;
- QJsonArray m_inputComboModel;
- QJsonArray m_outputComboModel;
- QJsonArray m_inputChannelsComboModel;
- QJsonArray m_outputChannelsComboModel;
- QJsonArray m_inputMixModeComboModel;
- QString m_devicesWarningMsg = QStringLiteral("");
- QString m_devicesErrorMsg = QStringLiteral("");
- QString m_devicesWarningHelpUrl = QStringLiteral("");
- QString m_devicesErrorHelpUrl = QStringLiteral("");
- bool m_highLatencyFlag = false;
-
- // other state not shared with QML
- QSharedPointer<VsPermissions> m_permissionsPtr;
- QScopedPointer<VsAudioWorker> m_audioWorkerPtr;
- QThread* m_workerThreadPtr;
- QTimer m_inputClipTimer;
- QTimer m_outputClipTimer;
- Meter* m_inputMeterPluginPtr;
- Meter* m_outputMeterPluginPtr;
- Volume* m_inputVolumePluginPtr;
- Volume* m_outputVolumePluginPtr;
- Monitor* m_monitorPluginPtr;
- bool mHasErrors; ///< true if one or more error callbacks have been triggered
-
-#ifndef NO_FEEDBACK
- Analyzer* m_outputAnalyzerPluginPtr;
-#endif
-
- QStringList m_audioBackendComboModel = {"JACK", "RtAudio"};
- QStringList m_feedbackDetectionComboModel = {"Enabled", "Disabled"};
- QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"};
- QStringList m_bufferStrategyComboModel = {
- "Adaptable Latency (Old)", "Stable Latency (Old)", "Loss Concealment (Default)"};
-
- friend class VsAudioWorker;
-};
-
-/// VsAudioWorker uses a separate thread to help VsAudio
-class VsAudioWorker : public QObject
-{
- Q_OBJECT
-
- public:
- VsAudioWorker(VsAudio* ptr);
- virtual ~VsAudioWorker() {}
-
- signals:
- void signalDevicesValidated();
- void signalUpdatedDeviceModels(QJsonArray inputComboModel,
- QJsonArray outputComboModel);
- void signalError(const QString& errorMessage);
-
- public slots:
- void openAudioInterface();
- void closeAudioInterface();
-#ifdef RT_AUDIO
- void refreshDevices();
- void validateDevices();
-
- private:
- void updateDeviceModels();
- void validateInputDevicesState();
- void validateOutputDevicesState();
- static void getDeviceList(const QVector<RtAudioDevice>& devices, QStringList& list,
- QStringList& categories, QList<int>& channels,
- bool isInput);
- static QJsonArray formatDeviceList(const QStringList& devices,
- const QStringList& categories,
- const QList<int>& channels);
- QVector<RtAudioDevice> m_devices;
-
- public:
- QVector<RtAudioDevice> getDevices() const { return m_devices; }
-#endif
-
- private:
- // parent getter wrappers
- bool getUseRtAudio() const { return m_parentPtr->getUseRtAudio(); }
- int getNumInputChannels() const { return m_parentPtr->getNumInputChannels(); }
- int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); }
- int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); }
- int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); }
- int getSampleRate() const { return m_parentPtr->getSampleRate(); }
- int getBufferSize() const { return m_parentPtr->getBufferSize(); }
- int getInputMixMode() const { return m_parentPtr->getInputMixMode(); }
- const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); }
- const QString& getOutputDevice() const { return m_parentPtr->getOutputDevice(); }
-
- VsAudio* m_parentPtr;
- QSharedPointer<AudioInterface> m_audioInterfacePtr;
- QList<int> m_inputDeviceChannels;
- QList<int> m_outputDeviceChannels;
- QStringList m_inputDeviceList;
- QStringList m_outputDeviceList;
-};
-
-#endif // VSDAUDIO_H
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsAuth.cpp
- * \author Dominick Hing
- * \date May 2023
- */
-
-#include "vsAuth.h"
-
-#include "./vsConstants.h"
-
-VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api)
- : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST)
-{
- m_networkAccessManager = networkAccessManager;
- m_api = api;
- m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager));
-
- connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
- &VsAuth::initializedCodeFlow);
- connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowError, this,
- &VsAuth::handleAuthFailed);
- connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::onCompletedCodeFlow, this,
- &VsAuth::codeFlowCompleted);
- connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this,
- &VsAuth::codeExpired);
-
- m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate");
-}
-
-void VsAuth::authenticate(QString currentRefreshToken)
-{
- if (currentRefreshToken.isEmpty()) {
- // if no refresh token, initialize device flow
- m_deviceCodeFlow->grant();
- } else {
- m_attemptingRefreshToken = true;
- emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
-
- // otherwise, use refresh token to gain a new access token
- m_refreshToken = currentRefreshToken;
- refreshAccessToken(m_refreshToken);
- }
-}
-
-void VsAuth::initializedCodeFlow(QString code, QString verificationUrl)
-{
- m_verificationCode = code;
- m_verificationUrl = verificationUrl;
- m_authenticationStage = QStringLiteral("polling");
-
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedVerificationUrl(m_verificationUrl);
-}
-
-void VsAuth::fetchUserInfo(QString accessToken)
-{
- QNetworkReply* reply = m_api->getAuth0UserInfo();
- connect(reply, &QNetworkReply::finished, this, [=]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "VsAuth::fetchUserInfo Error: "
- << reply->errorString().toStdString() << std::endl;
- handleAuthFailed(); // handle failure
- emit fetchUserInfoFailed();
- reply->deleteLater();
- return;
- }
-
- QByteArray response = reply->readAll();
- QJsonDocument userInfo = QJsonDocument::fromJson(response);
- QString userId = userInfo.object()[QStringLiteral("sub")].toString();
-
- reply->deleteLater();
-
- if (userId.isEmpty()) {
- std::cout << "VsAuth::fetchUserInfo Error: empty userId" << std::endl;
- handleAuthFailed(); // handle failure
- emit fetchUserInfoFailed();
- return;
- }
-
- handleAuthSucceeded(userId, accessToken);
- });
-}
-
-void VsAuth::refreshAccessToken(QString refreshToken)
-{
- qDebug() << "Refreshing access token";
- m_authenticationStage = QStringLiteral("refreshing");
- emit updatedAuthenticationStage(m_authenticationStage);
-
- QNetworkRequest request = QNetworkRequest(
- QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
-
- request.setRawHeader(QByteArray("Content-Type"),
- QByteArray("application/x-www-form-urlencoded"));
-
- QString data = QString("grant_type=refresh_token&client_id=%1&refresh_token=%2")
- .arg(m_clientId, refreshToken);
-
- // send request
- QNetworkReply* reply = m_networkAccessManager->post(request, data.toUtf8());
-
- connect(reply, &QNetworkReply::finished, this, [=]() {
- QByteArray buffer = reply->readAll();
-
- // Error: failed to get device code
- if (reply->error()) {
- std::cout << "Failed to get new access token: " << buffer.toStdString()
- << std::endl;
- handleAuthFailed(); // handle failure
- emit refreshTokenFailed();
- reply->deleteLater();
- return;
- }
-
- // parse JSON from string response
- QJsonParseError parseError;
- QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
- if (parseError.error) {
- std::cout << "Error parsing JSON for Access Token: "
- << parseError.errorString().toStdString() << std::endl;
- handleAuthFailed(); // handle failure
- emit refreshTokenFailed();
- reply->deleteLater();
- return;
- }
-
- // received access token
- QJsonObject object = data.object();
- QString accessToken = object.value(QLatin1String("access_token")).toString();
- m_api->setAccessToken(accessToken); // set access token
- reply->deleteLater();
- if (m_userId.isEmpty()) {
- fetchUserInfo(accessToken); // get user ID from Auth0
- } else {
- handleRefreshSucceeded(accessToken);
- }
- });
-}
-
-void VsAuth::resetCode()
-{
- if (!m_verificationCode.isEmpty()) {
- m_deviceCodeFlow->cancelCodeFlow();
- m_deviceCodeFlow->grant();
- }
-}
-
-void VsAuth::codeFlowCompleted(QString accessToken, QString refreshToken)
-{
- m_refreshToken = refreshToken;
- m_api->setAccessToken(accessToken);
- fetchUserInfo(accessToken);
-}
-
-void VsAuth::codeExpired()
-{
- emit deviceCodeExpired();
-}
-
-void VsAuth::handleRefreshSucceeded(QString accessToken)
-{
- qDebug() << "Successfully refreshed access token";
-
- m_accessToken = accessToken;
- m_authenticationStage = QStringLiteral("success");
- m_attemptingRefreshToken = false;
-
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
-}
-
-void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
-{
- // Success case: we got our access token (either through the refresh token or device
- // code flow), and fetched the user ID
- std::cout << "Successfully authenticated Virtual Studio user" << std::endl;
- std::cout << "User ID: " << userId.toStdString() << std::endl;
-
- if (m_authenticationStage == QStringLiteral("polling")) {
- m_authenticationMethod = QStringLiteral("code flow");
- } else {
- m_authenticationMethod = QStringLiteral("refresh token");
- }
-
- m_userId = userId;
- m_verificationCode = QStringLiteral("");
- m_accessToken = accessToken;
- m_authenticationStage = QStringLiteral("success");
- m_attemptingRefreshToken = false;
- m_isAuthenticated = true;
-
- emit updatedUserId(m_userId);
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedIsAuthenticated(m_isAuthenticated);
- emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
- emit updatedAuthenticationMethod(m_authenticationMethod);
-
- // notify UI and virtual studio class of success
- emit authSucceeded();
-}
-
-void VsAuth::handleAuthFailed()
-{
- // this might get called because there was an error getting the access token,
- // or there was an issue fetching the user ID. We need both to say
- // that authentication succeeded
- std::cout << "Failed to authenticate user" << std::endl;
-
- m_userId = QStringLiteral("");
- m_verificationCode = QStringLiteral("");
- m_accessToken = QStringLiteral("");
- m_authenticationStage = QStringLiteral("failed");
- m_authenticationMethod = QStringLiteral("");
- m_attemptingRefreshToken = false;
- m_isAuthenticated = false;
-
- emit updatedUserId(m_userId);
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedIsAuthenticated(m_isAuthenticated);
- emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
- emit updatedAuthenticationMethod(m_authenticationMethod);
-
- // notify UI and virtual studio class of failure
- emit authFailed();
-}
-
-void VsAuth::cancelAuthenticationFlow()
-{
- qDebug() << "Canceling authentication flow";
- m_deviceCodeFlow->cancelCodeFlow();
-
- m_userId = QStringLiteral("");
- m_verificationCode = QStringLiteral("");
- m_accessToken = QStringLiteral("");
- m_authenticationStage = QStringLiteral("unauthenticated");
- m_isAuthenticated = false;
-
- emit updatedUserId(m_userId);
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedIsAuthenticated(m_isAuthenticated);
-}
-
-void VsAuth::logout()
-{
- if (!m_isAuthenticated) {
- std::cout << "Warning: attempting to logout while not authenticated" << std::endl;
- }
- qDebug() << "Logging out";
-
- // reset auth state
- m_userId = QStringLiteral("");
- m_verificationCode = QStringLiteral("");
- m_accessToken = QStringLiteral("");
- m_authenticationStage = QStringLiteral("unauthenticated");
- m_isAuthenticated = false;
-
- emit updatedUserId(m_userId);
- emit updatedAuthenticationStage(m_authenticationStage);
- emit updatedVerificationCode(m_verificationCode);
- emit updatedIsAuthenticated(m_isAuthenticated);
-}
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsAuth.h
- * \author Dominick Hing
- * \date May 2023
- */
-
-#ifndef VSAUTH_H
-#define VSAUTH_H
-
-#include <QNetworkAccessManager>
-#include <QQmlContext>
-#include <QQmlEngine>
-#include <QString>
-#include <iostream>
-
-#include "vsApi.h"
-#include "vsDeviceCodeFlow.h"
-
-class VsAuth : public QObject
-{
- Q_OBJECT
-
- Q_PROPERTY(QString authenticationStage READ authenticationStage NOTIFY
- updatedAuthenticationStage);
- Q_PROPERTY(QString verificationCode READ deviceCode NOTIFY updatedVerificationCode);
- Q_PROPERTY(
- QString verificationUrl READ deviceVerificationUrl NOTIFY updatedVerificationUrl);
- Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY updatedIsAuthenticated);
- Q_PROPERTY(QString authenticationMethod READ authenticationMethod NOTIFY
- updatedAuthenticationMethod);
- Q_PROPERTY(bool attemptingRefreshToken READ attemptingRefreshToken NOTIFY
- updatedAttemptingRefreshToken);
- Q_PROPERTY(QString userId READ userId NOTIFY updatedUserId);
- Q_PROPERTY(QString accessToken READ accessToken CONSTANT);
-
- public:
- VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api);
-
- void authenticate(QString currentRefreshToken);
- void refreshAccessToken(QString refreshToken);
- Q_INVOKABLE void resetCode();
- void logout();
-
- public slots:
- void cancelAuthenticationFlow();
-
- // getter methods
- QString authenticationStage() { return m_authenticationStage; };
- QString deviceCode() { return m_verificationCode; };
- QString deviceVerificationUrl() { return m_verificationUrl; };
- bool isAuthenticated() { return m_isAuthenticated; };
- QString userId() { return m_userId; };
- QString accessToken() { return m_accessToken; };
- QString refreshToken() { return m_refreshToken; };
- QString authenticationMethod() { return m_authenticationMethod; }
- bool attemptingRefreshToken() { return m_attemptingRefreshToken; }
-
- signals:
- void updatedAuthenticationStage(QString authenticationStage);
- void updatedVerificationCode(QString deviceCode);
- void updatedVerificationUrl(QUrl verificationUrl);
- void updatedIsAuthenticated(bool isAuthenticated);
- void updatedUserId(QString userId);
- void updatedAuthenticationMethod(QString grant);
- void updatedAttemptingRefreshToken(bool attemptingRefreshToken);
- void authSucceeded();
- void authFailed();
- void refreshTokenFailed();
- void fetchUserInfoFailed();
- void deviceCodeExpired();
-
- private slots:
- void handleRefreshSucceeded(QString accessToken);
- void handleAuthSucceeded(QString userId, QString accessToken);
- void handleAuthFailed();
- void initializedCodeFlow(QString code, QString verificationUrl);
- void codeFlowCompleted(QString accessToken, QString refreshToken);
- void codeExpired();
-
- private:
- void fetchUserInfo(QString accessToken);
-
- QString m_clientId;
- QString m_authorizationServerHost;
-
- QString m_authenticationStage = QStringLiteral("unauthenticated");
- QString m_verificationCode = QStringLiteral("");
- QString m_verificationUrl;
- QString m_authenticationMethod = QStringLiteral("");
-
- bool m_attemptingRefreshToken = false;
- bool m_isAuthenticated = false;
- QString m_userId;
- QString m_accessToken;
- QString m_refreshToken;
-
- QNetworkAccessManager* m_networkAccessManager;
- VsApi* m_api;
- QScopedPointer<VsDeviceCodeFlow> m_deviceCodeFlow;
-};
-
-#endif
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsConstants.h
- * \author Nelson Wang
- * \date Oct 2022
- */
-
-#ifndef VSCONSTANTS_H
-#define VSCONSTANTS_H
-
-#include <QString>
-
-const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize");
-const QString AUTH_TOKEN_URI = QStringLiteral("https://auth.jacktrip.org/oauth/token");
-const QString AUTH_AUDIENCE = QStringLiteral("https://api.jacktrip.org");
-const QString AUTH_CLIENT_ID = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I");
-const QString PROD_API_HOST = QStringLiteral("app.jacktrip.com");
-const QString TEST_API_HOST = QStringLiteral("test.jacktrip.com");
-const QString AUTH_SERVER_HOST = QStringLiteral("auth.jacktrip.org");
-
-#endif // VSCONSTANTS_H
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2023 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 vsDeeplink.cpp
- * \author Aaron Wyatt, based on code by Matt Horton
- * \date February 2023
- */
-
-#include "vsDeeplink.h"
-
-#include <QCoreApplication>
-#include <QDebug>
-#include <QDesktopServices>
-#include <QDir>
-#include <QEventLoop>
-#include <QMutexLocker>
-#include <QSettings>
-#include <QTimer>
-
-VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink)
-{
- setUrlScheme();
- checkForInstance();
- QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl");
-}
-
-VsDeeplink::~VsDeeplink()
-{
- QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip"));
-}
-
-bool VsDeeplink::waitForReady()
-{
- while (!m_isReady) {
- QTimer timer;
- timer.setTimerType(Qt::CoarseTimer);
- timer.setSingleShot(true);
-
- QEventLoop loop;
- QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit);
- QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
- timer.start(100); // wait for 100ms
- loop.exec();
- }
- return m_readyToExit;
-}
-
-void VsDeeplink::readyForSignals()
-{
- m_readyForSignals = true;
- if (!m_deeplink.isEmpty()) {
- emit signalDeeplink(m_deeplink);
- m_deeplink.clear();
- }
-}
-
-void VsDeeplink::handleUrl(const QUrl& url)
-{
- if (m_readyForSignals) {
- emit signalDeeplink(url);
- } else {
- m_deeplink = url;
- }
-}
-
-void VsDeeplink::checkForInstance()
-{
- // Create socket
- m_instanceCheckSocket.reset(new QLocalSocket(this));
- QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this,
- &VsDeeplink::connectionReceived, Qt::QueuedConnection);
- // Create instanceServer to prevent new instances from being created
- void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError);
-#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
- errorFunc = &QLocalSocket::error;
-#else
- errorFunc = &QLocalSocket::errorOccurred;
-#endif
- QObject::connect(m_instanceCheckSocket.data(), errorFunc, this,
- &VsDeeplink::connectionFailed);
- // Check for existing instance
- m_instanceCheckSocket->connectToServer("jacktripExists");
-}
-
-void VsDeeplink::connectionReceived()
-{
- // another jacktrip instance is running
- if (!m_deeplink.isEmpty()) {
- // pass deeplink to existing instance before quitting
- QString deeplinkStr = m_deeplink.toString();
- QByteArray baDeeplink = deeplinkStr.toLocal8Bit();
- qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink);
- if (writeBytes < 0) {
- qDebug() << "sending deeplink failed";
- } else {
- qDebug() << "Sent deeplink request to remote instance";
- }
-
- // make sure it isn't processed again
- m_deeplink.clear();
-
- // End process if another instance exists
- m_readyToExit = true;
- }
-
- m_instanceCheckSocket->waitForBytesWritten();
- m_instanceCheckSocket->disconnectFromServer(); // remove next
-
- // let main thread know we are finished
- m_isReady = true;
- emit signalIsReady();
-}
-
-void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError)
-{
- switch (socketError) {
- case QLocalSocket::ServerNotFoundError:
- case QLocalSocket::SocketTimeoutError:
- case QLocalSocket::ConnectionRefusedError:
- // no other jacktrip instance is running, so we will take over handling deep links
- qDebug() << "Listening for deep link requests";
- m_instanceServer.reset(new QLocalServer(this));
- m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
- m_instanceServer->listen("jacktripExists");
- QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
- &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection);
- break;
- case QLocalSocket::PeerClosedError:
- break;
- default:
- qDebug() << m_instanceCheckSocket->errorString();
- }
-
- // let main thread know we are finished
- m_isReady = true;
- emit signalIsReady();
-}
-
-void VsDeeplink::handleDeeplinkRequest()
-{
- while (m_instanceServer->hasPendingConnections()) {
- // Receive URL from 2nd instance
- QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
-
- if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
- qDebug() << "Deeplink socket: never received connection";
- return;
- }
-
- if (!connectedSocket->waitForReadyRead()
- && connectedSocket->bytesAvailable() <= 0) {
- qDebug() << "Deeplink socket: not ready and no bytes available: "
- << connectedSocket->errorString();
- return;
- }
-
- if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
- qDebug() << "Deeplink socket: ready but no bytes available";
- break;
- }
-
- QByteArray in(connectedSocket->readAll());
- QString urlString(in);
- handleUrl(urlString);
- }
-}
-
-void VsDeeplink::setUrlScheme()
-{
-#ifdef _WIN32
- // Set url scheme in registry
- QString path = QDir::toNativeSeparators(qApp->applicationFilePath());
-
- QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat);
- set.beginGroup("jacktrip");
- set.setValue("Default", "URL:JackTrip Protocol");
- set.setValue("DefaultIcon/Default", path);
- set.setValue("URL Protocol", "");
- set.setValue("shell/open/command/Default",
- QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\"");
- set.endGroup();
-#endif
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2023 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 vsDeeplink.h
- * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
- * \date August 2023
- */
-
-#ifndef __VSDEEPLINK_H__
-#define __VSDEEPLINK_H__
-
-#include <QLocalServer>
-#include <QLocalSocket>
-#include <QScopedPointer>
-#include <QString>
-#include <QUrl>
-
-class VsDeeplink : public QObject
-{
- Q_OBJECT
-
- public:
- // construct with an instance of the application, to parse command line args
- VsDeeplink(const QString& deeplink);
-
- // virtual destructor since it inherits from QObject
- // this is used to unregister url handler
- virtual ~VsDeeplink();
-
- // blocks main thread until local socket server is ready
- // returns true if a deeplink was handled and we should exit now
- bool waitForReady();
-
- // used to let us know VirtualStudio is ready to process deeplink signals
- void readyForSignals();
-
- // returns deeplink extracted from command line, if any
- const QUrl& getDeeplink() const { return m_deeplink; }
-
- signals:
-
- // signalIsReady is emitted when the local socket server is ready
- void signalIsReady();
-
- // signalDeeplink is emitted when we want the local instance to process a deeplink
- void signalDeeplink(const QUrl& url);
-
- private slots:
-
- // handleUrl is called to trigger processing of a deeplink
- void handleUrl(const QUrl& url);
-
- // checks to see if another instance of jacktrip is available to process requests.
- // if there is, this will send any command line deeplinks to it and exit.
- // if there isn't, this will start listening for requests.
- void checkForInstance();
-
- // called if a connection was established with another instance of VS
- void connectionReceived();
-
- // called if unable to connect to another instance of VS
- void connectionFailed(QLocalSocket::LocalSocketError socketError);
-
- // called by local socket server to process deeplink requests
- void handleDeeplinkRequest();
-
- private:
- // sets url scheme for windows machines; does nothing on other platforms
- static void setUrlScheme();
-
- // used to check if there is a virtual studio instance already running
- QScopedPointer<QLocalSocket> m_instanceCheckSocket;
-
- // used to listen for deeplink requests via local socket connections
- QScopedPointer<QLocalServer> m_instanceServer;
-
- // used to synchronize with main thread at startup
- bool m_isReady = false;
- bool m_readyForSignals = false;
- bool m_readyToExit = false;
- QUrl m_deeplink;
-};
-
-#endif // __VSDEEPLINK_H__
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsDevice.cpp
- * \author Matt Horton
- * \date June 2022
- */
-
-#include "vsDevice.h"
-
-#include <QEventLoop>
-
-// Constructor
-VsDevice::VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
- QSharedPointer<VsAudio>& audio, QObject* parent)
- : QObject(parent)
- , m_auth(auth)
- , m_api(api)
- , m_audioConfigPtr(audio)
- , m_sendVolumeTimer(this)
-{
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- m_apiPrefix = settings.value(QStringLiteral("ApiPrefix"), "").toString();
- m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString();
- m_appUUID = settings.value(QStringLiteral("AppUUID"), "").toString();
- m_appID = settings.value(QStringLiteral("AppID"), "").toString();
- settings.endGroup();
-
- if (!m_appID.isEmpty()) {
- std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
- }
-
- m_sendVolumeTimer.setSingleShot(true);
- connect(&m_sendVolumeTimer, &QTimer::timeout, this, &VsDevice::sendLevels);
-
- // Set server levels to stored versions
- sendLevels();
-}
-
-VsDevice::~VsDevice()
-{
- m_sendVolumeTimer.stop();
- stopJackTrip(false);
-}
-
-// registerApp idempotently registers an emulated device belonging to the current user
-void VsDevice::registerApp()
-{
- if (m_appUUID == "") {
- m_appUUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces);
- }
-
- // check if device exists
- QNetworkReply* reply = m_api->getDevice(m_appID);
- connect(reply, &QNetworkReply::finished, this, [=]() {
- // Got error
- if (reply->error() != QNetworkReply::NoError) {
- QVariant statusCode =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
- if (!statusCode.isValid()) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- }
-
- int status = statusCode.toInt();
- // Device does not exist
- if (status >= 400 && status < 500) {
- std::cout << "Device not found. Creating new device." << std::endl;
-
- if (m_apiPrefix == "" || m_apiSecret == "") {
- m_apiPrefix = randomString(7);
- m_apiSecret = randomString(22);
- }
-
- registerJTAsDevice();
- } else {
- // Other error status. Won't create device.
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- }
- } else if (m_apiPrefix != "" && m_apiSecret != "") {
- sendHeartbeat();
- }
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("AppUUID"), m_appUUID);
- settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix);
- settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret);
- settings.endGroup();
- if (!m_appID.isEmpty()) {
- updateState("");
- }
-
- reply->deleteLater();
- });
-}
-
-// removeApp deletes the emulated device
-void VsDevice::removeApp()
-{
- if (m_appID.isEmpty()) {
- return;
- }
-
- QNetworkReply* reply = m_api->deleteDevice(m_appID);
- QEventLoop loop;
- connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
- loop.exec();
-
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- } else {
- m_appID.clear();
- m_appUUID.clear();
- m_apiPrefix.clear();
- m_apiSecret.clear();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.remove(QStringLiteral("AppID"));
- settings.remove(QStringLiteral("AppUUID"));
- settings.remove(QStringLiteral("ApiPrefix"));
- settings.remove(QStringLiteral("ApiSecret"));
- settings.endGroup();
- }
- reply->deleteLater();
-}
-
-// sendHeartbeat is reponsible for sending liveness heartbeats to the API
-void VsDevice::sendHeartbeat()
-{
- QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
-
- QJsonObject json = {
- {QLatin1String("stats_updated_at"), now},
- {QLatin1String("mac"), m_appUUID},
- {QLatin1String("version"), QLatin1String(gVersion)},
- {QLatin1String("type"), "jacktrip_app"},
- {QLatin1String("apiPrefix"), m_apiPrefix},
- {QLatin1String("apiSecret"), m_apiSecret},
- };
-
- // Add stats to heartbeat body
- if (!m_pinger.isNull() && m_pinger->active()) {
- VsPinger::PingStat stats = m_pinger->getPingStats();
-
- // API server expects RTTs to be in int64 nanoseconds, so we must convert
- // from milliseconds to nanoseconds
- int ns_per_ms = 1000000;
-
- json.insert(QLatin1String("pkts_sent"), (int)stats.packetsSent);
- json.insert(QLatin1String("pkts_recv"), (int)stats.packetsReceived);
- json.insert(QLatin1String("min_rtt"), (qint64)(stats.minRtt * ns_per_ms));
- json.insert(QLatin1String("max_rtt"), (qint64)(stats.maxRtt * ns_per_ms));
- json.insert(QLatin1String("avg_rtt"), (qint64)(stats.avgRtt * ns_per_ms));
- 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 = {};
- pingStats.insert(QLatin1String("packetsSent"), (int)stats.packetsSent);
- pingStats.insert(QLatin1String("packetsReceived"), (int)stats.packetsReceived);
- pingStats.insert(QLatin1String("minRtt"), ((int)(10 * stats.minRtt)) / 10.0);
- pingStats.insert(QLatin1String("maxRtt"), ((int)(10 * stats.maxRtt)) / 10.0);
- pingStats.insert(QLatin1String("avgRtt"), ((int)(10 * stats.avgRtt)) / 10.0);
- pingStats.insert(QLatin1String("stdDevRtt"),
- ((int)(10 * stats.stdDevRtt)) / 10.0);
- pingStats.insert(QLatin1String("highLatency"),
- m_audioConfigPtr->getHighLatencyFlag());
- emit updateNetworkStats(pingStats);
- }
-
- QJsonDocument request = QJsonDocument(json);
-
- if (!m_deviceSocketPtr.isNull() && m_deviceSocketPtr->isValid()) {
- // Send heartbeat via websocket
- m_deviceSocketPtr->sendMessage(request.toJson());
- } else {
- if (enabled()) {
- if (m_deviceSocketPtr.isNull()) {
- qDebug() << "Heartbeat not sent";
- } else {
- qDebug() << "Heartbeat not sent; trying to reopen socket";
- m_deviceSocketPtr->openSocket();
- }
- }
- }
-}
-
-bool VsDevice::hasTerminated()
-{
- return m_jackTrip.isNull();
-}
-
-// updateState updates the emulated device with the provided state
-void VsDevice::updateState(const QString& serverId)
-{
- m_deviceAgentConfig.insert("serverId", serverId);
- m_deviceAgentConfig.insert("enabled", !serverId.isEmpty());
- QJsonObject json = {
- {QLatin1String("serverId"), serverId},
- {QLatin1String("enabled"), !serverId.isEmpty()},
- };
- QJsonDocument request = QJsonDocument(json);
- QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- }
- reply->deleteLater();
- });
-}
-
-void VsDevice::sendLevels()
-{
- if (m_appID.isEmpty()) {
- return;
- }
- // Add latest volume and mute values to heartbeat body
- QJsonObject json = {{QLatin1String("version"), QLatin1String(gVersion)},
- {QLatin1String("captureVolume"),
- (int)(m_audioConfigPtr->getInputVolume() * 100.0)},
- {QLatin1String("captureMute"), m_audioConfigPtr->getInputMuted()},
- {QLatin1String("playbackVolume"),
- (int)(m_audioConfigPtr->getOutputVolume() * 100.0)},
- {QLatin1String("playbackMute"), false},
- {QLatin1String("monitorVolume"),
- (int)(m_audioConfigPtr->getMonitorVolume() * 100.0)}};
-
- QJsonDocument request = QJsonDocument(json);
- QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- }
- reply->deleteLater();
- });
-}
-
-// initJackTrip spawns a new jacktrip process with the desired settings
-JackTrip* VsDevice::initJackTrip(
- [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input,
- [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel,
- [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel,
- [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode,
- [[maybe_unused]] int bufferSize, [[maybe_unused]] int bufferStrategy,
- VsServerInfo* studioInfo)
-{
- m_jackTrip.reset(
- new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel,
- numChannelsIn, baseOutputChannel, numChannelsOut,
- static_cast<AudioInterface::inputMixModeT>(inputMixMode),
-#ifdef WAIR // wair
- 0,
-#endif // endwhere
- 4, 1));
- m_jackTrip->setConnectDefaultAudioPorts(true);
-#ifdef RT_AUDIO
- if (useRtAudio) {
- m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
- m_jackTrip->setAudioBufferSizeInSamples(bufferSize);
- m_jackTrip->setInputDevice(input);
- m_jackTrip->setOutputDevice(output);
- }
-#endif
- m_jackTrip->setSampleRate(studioInfo->sampleRate());
- int bindPort = selectBindPort();
- if (bindPort == 0) {
- return 0;
- }
- m_jackTrip->setBindPorts(bindPort);
- m_jackTrip->setRemoteClientName(m_appID);
- 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());
-
- QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this,
- &VsDevice::handleJackTripError, Qt::QueuedConnection);
- QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this,
- &VsDevice::handleJackTripError, Qt::QueuedConnection);
-
- return m_jackTrip.data();
-}
-
-// startJackTrip starts the current jacktrip process if applicable
-void VsDevice::startJackTrip(const VsServerInfo& studioInfo)
-{
- m_stopping = false;
- m_networkOutage = false;
- updateState(studioInfo.id());
-
- // setup websocket listener
- m_deviceSocketPtr.reset(
- new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/devices/%2/heartbeat")
- .arg(m_api->getApiHost(), m_appID)),
- m_auth->accessToken(), m_apiPrefix, m_apiSecret));
- connect(m_deviceSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
- &VsDevice::onTextMessageReceived);
- connect(m_deviceSocketPtr.get(), &VsWebSocket::disconnected, this,
- &VsDevice::restartDeviceSocket);
- m_deviceSocketPtr->openSocket();
-
- if (!m_jackTrip.isNull()) {
-#ifdef WAIRTOHUB // WAIR
- m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name
-#else
- m_jackTrip->startProcess();
-#endif // endwhere
- }
-
- // intialize the pinger used to generate network latency statistics for
- // Virtual Studio
- QString host = studioInfo.host();
- if (studioInfo.isManaged()) {
- host = studioInfo.sessionId();
- host.append(QString::fromStdString(".jacktrip.cloud"));
- }
- if (studioInfo.isManaged() || !studioInfo.sessionId().isEmpty()) {
- m_pinger.reset(new VsPinger(QString::fromStdString("wss"), host,
- QString::fromStdString("/ping")));
- }
-}
-
-// stopJackTrip stops the current jacktrip process if applicable
-void VsDevice::stopJackTrip(bool isReconnecting)
-{
- // check if another process has already initiated
- QMutexLocker stopLock(&m_stopMutex);
- if (m_stopping)
- return;
- m_stopping = true;
-
- // only clear state if we are not reconnecting
- if (!isReconnecting)
- updateState("");
-
- // stop the Virtual Studio pinger
- if (!m_pinger.isNull()) {
- m_pinger->stop();
- m_pinger->unsetToken();
- }
-
- if (!m_jackTrip.isNull()) {
- if (!m_deviceSocketPtr.isNull()) {
- m_deviceSocketPtr->closeSocket();
- }
- m_jackTrip->stop();
- m_jackTrip.reset();
- }
-}
-
-// reconcileAgentConfig updates the internal DeviceAgentConfig structure
-void VsDevice::reconcileAgentConfig(QJsonDocument newState)
-{
- // Only sync if the incoming type matches DeviceAgentConfig:
- // https://github.com/jacktrip/jacktrip-agent/blob/fd3940c293daf16d8467c62b39a30779d21a0a22/pkg/client/devices.go#L87
- QJsonObject newObject = newState.object();
- if (!newObject.contains("enabled")) {
- return;
- }
- for (auto it = newObject.constBegin(); it != newObject.constEnd(); it++) {
- // if currently enabled but new config is not enabled, disconnect immediately
- if (enabled() && it.key() == "enabled" && !it.value().toBool()
- && !m_jackTrip.isNull()) {
- stopJackTrip(false);
- }
- m_deviceAgentConfig.insert(it.key(), it.value());
- }
-}
-
-// syncDeviceSettings updates volume/mute controls against the API
-void VsDevice::syncDeviceSettings()
-{
- m_sendVolumeTimer.start(100);
-}
-
-// handleJackTripError is a slot intended to be triggered on jacktrip process signals
-void VsDevice::handleJackTripError()
-{
- stopJackTrip(false);
-}
-
-// onTextMessageReceived is a slot intended to be triggered by new incoming WSS messages
-void VsDevice::onTextMessageReceived(const QString& message)
-{
- QJsonDocument newState = QJsonDocument::fromJson(message.toUtf8());
- QJsonObject newObj = newState.object();
-
- // We have a heartbeat from which we can read the studio auth token
- // Use it to set up and start the pinger connection
- QString token = newState["authToken"].toString();
- if (!m_pinger.isNull() && !m_pinger->active() && !token.isEmpty()) {
- m_pinger->setToken(token);
- m_pinger->start();
- }
-
- // capture (input) volume
- m_audioConfigPtr->setInputVolume(
- (float)(newObj[QStringLiteral("captureVolume")].toDouble() / 100.0));
- m_audioConfigPtr->setInputMuted(newObj[QStringLiteral("captureMute")].toBool());
-
- // playback (output) volume
- m_audioConfigPtr->setOutputVolume(
- (float)(newObj[QStringLiteral("playbackVolume")].toDouble() / 100.0));
-
- // monitor volume
- m_audioConfigPtr->setMonitorVolume(
- (float)(newObj[QStringLiteral("monitorVolume")].toDouble() / 100.0));
-
- reconcileAgentConfig(newState);
-}
-
-void VsDevice::restartDeviceSocket()
-{
- if (m_deviceAgentConfig[QStringLiteral("serverId")].toString() != "") {
- if (!m_deviceSocketPtr.isNull()) {
- m_deviceSocketPtr->openSocket();
- }
- }
-}
-
-// registerJTAsDevice creates the emulated device belonging to the current user
-void VsDevice::registerJTAsDevice()
-{
- /*
- REGISTER JT APP AS A DEVICE ON VIRTUAL STUDIO
-
- Defaults:
- period - 128 - set by studio = buffer size
- queueBuffer - 0 - set by studio = net queue
- devicePort - 4464
- reverb - 0 - off
- limiter - false
- compressor - false
- quality - 2 - high
- captureMute - false - unused right now
- captureVolume - 100 - unused right now
- playbackMute - false - unused right now
- playbackVolume - 100 - unused right now
- monitorMute - false - unsure if we should enable
- monitorVolume - 0 - unsure if we should enable
- name - "JackTrip App"
- alsaName - "jacktripapp"
- overlay - "jacktrip_app"
- mac - UUID tied to app session
- version - app version - will need to update in heartbeat
- apiPrefix - random 7 character string tied to app session
- apiSecret - random 22 character string tied to app session
- */
-
- QJsonObject json = {
- // TODO: Fix me
- //{QLatin1String("period"), m_bufferOptions[bufferSize()].toInt()},
- {QLatin1String("period"), 128},
- {QLatin1String("queueBuffer"), 0},
- {QLatin1String("devicePort"), 4464},
- {QLatin1String("reverb"), 0},
- {QLatin1String("limiter"), false},
- {QLatin1String("compressor"), false},
- {QLatin1String("quality"), 2},
- {QLatin1String("captureMute"), false},
- {QLatin1String("captureVolume"), 100},
- {QLatin1String("playbackMute"), false},
- {QLatin1String("playbackVolume"), 100},
- {QLatin1String("monitorMute"), false},
- {QLatin1String("monitorVolume"), 100},
- {QLatin1String("alsaName"), "jacktripapp"},
- {QLatin1String("overlay"), "jacktrip_app"},
- {QLatin1String("mac"), m_appUUID},
- {QLatin1String("version"), QLatin1String(gVersion)},
- {QLatin1String("apiPrefix"), m_apiPrefix},
- {QLatin1String("apiSecret"), m_apiSecret},
-#if defined(Q_OS_MACOS)
- {QLatin1String("name"), "JackTrip App (macOS)"},
-#elif defined(Q_OS_WIN)
- {QLatin1String("name"), "JackTrip App (Windows)"},
-#else
- {QLatin1String("name"), "JackTrip App"},
-#endif // Q_OS_WIN
- };
- QJsonDocument request = QJsonDocument(json);
-
- QNetworkReply* reply = m_api->postDevice(request.toJson());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- if (reply->error() != QNetworkReply::NoError) {
- std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
- reply->deleteLater();
- return;
- } else {
- QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
- QJsonObject newObject = response.object();
-
- m_appID = newObject[QStringLiteral("id")].toString();
-
- // capture (input) volume
- m_audioConfigPtr->setInputVolume(
- (float)(newObject[QStringLiteral("captureVolume")].toDouble() / 100.0));
- m_audioConfigPtr->setInputMuted(
- newObject[QStringLiteral("captureMute")].toBool());
-
- // playback (output) volume
- m_audioConfigPtr->setOutputVolume(
- (float)(newObject[QStringLiteral("playbackVolume")].toDouble() / 100.0));
-
- // monitor volume
- m_audioConfigPtr->setMonitorVolume(
- (float)(newObject[QStringLiteral("monitorVolume")].toDouble() / 100.0));
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("AppID"), m_appID);
- settings.endGroup();
-
- std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
- sendHeartbeat();
- }
-
- reply->deleteLater();
- });
-}
-
-// enabled returns whether or not the client is connected to a studio
-bool VsDevice::enabled()
-{
- return m_deviceAgentConfig[QStringLiteral("enabled")].toBool();
-}
-
-// randomString generates a random sequence of characters
-QString VsDevice::randomString(int stringLength)
-{
- QString str = "";
- static bool seeded = false;
- QString allow_symbols(
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
-
- if (!seeded) {
- m_randomizer.seed((QTime::currentTime().msec()));
- seeded = true;
- }
-
- for (int i = 0; i < stringLength; ++i) {
- str.append(allow_symbols.at(m_randomizer.generate() % (allow_symbols.length())));
- }
-
- return str;
-}
-
-// selectBindPort finds the next open bind port to use for jacktrip
-int VsDevice::selectBindPort()
-{
- int candidate = gDefaultPort;
- if (m_jackTrip.isNull()) {
- return candidate;
- }
- int attempt = 0;
- while (attempt <= 5000) {
- candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1);
- attempt++;
- if (!m_jackTrip->checkIfPortIsBinded(candidate)) {
- return candidate;
- }
- }
- return 0;
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsDevice.h
- * \author Matt Horton
- * \date June 2022
- */
-
-#ifndef VSDEVICE_H
-#define VSDEVICE_H
-
-#include <QMutex>
-#include <QObject>
-#include <QString>
-#include <QTimer>
-#include <QUuid>
-#include <QtWebSockets>
-
-#include "../JackTrip.h"
-#include "../jacktrip_globals.h"
-#include "vsApi.h"
-#include "vsAudio.h"
-#include "vsAuth.h"
-#include "vsConstants.h"
-#include "vsPinger.h"
-#include "vsServerInfo.h"
-#include "vsWebSocket.h"
-
-class VsDevice : public QObject
-{
- Q_OBJECT
-
- public:
- // Constructor
- explicit VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
- QSharedPointer<VsAudio>& audio, QObject* parent = nullptr);
- virtual ~VsDevice();
-
- // Public functions
- void registerApp();
- void removeApp();
- void sendHeartbeat();
- bool hasTerminated();
- JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output,
- int baseInputChannel, int numChannelsIn, int baseOutputChannel,
- int numChannelsOut, int inputMixMode, int bufferSize,
- int bufferStrategy, VsServerInfo* studioInfo);
- 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);
-
- public slots:
- void syncDeviceSettings();
-
- private slots:
- void handleJackTripError();
- void onTextMessageReceived(const QString& message);
- void restartDeviceSocket();
- void sendLevels();
-
- private:
- void updateState(const QString& serverId);
- void registerJTAsDevice();
- bool enabled();
- int selectBindPort();
- QString randomString(int stringLength);
-
- QSharedPointer<VsAuth> m_auth;
- QSharedPointer<VsApi> m_api;
- QSharedPointer<VsAudio> m_audioConfigPtr;
- QScopedPointer<VsPinger> m_pinger;
-
- QString m_appID;
- QString m_appUUID;
- QString m_token;
- QString m_apiPrefix;
- QString m_apiSecret;
- QMutex m_stopMutex;
- QJsonObject m_deviceAgentConfig;
- QScopedPointer<VsWebSocket> m_deviceSocketPtr;
- QScopedPointer<JackTrip> m_jackTrip;
- QRandomGenerator m_randomizer;
- QTimer m_sendVolumeTimer;
- bool m_networkOutage = false;
- bool m_stopping = false;
-};
-
-#endif // VSDEVICE_H
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsDeviceCodeFlow.cpp
- * \author Dominick Hing
- * \date May 2023
- */
-
-#include "./vsDeviceCodeFlow.h"
-
-#include "./vsConstants.h"
-
-VsDeviceCodeFlow::VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager)
- : m_clientId(AUTH_CLIENT_ID)
- , m_audience(AUTH_AUDIENCE)
- , m_authorizationServerHost(AUTH_SERVER_HOST)
- , m_authenticationError(false)
- , m_netManager(networkAccessManager)
-{
- // start polling when the device flow has been initialized
- connect(this, &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
- &VsDeviceCodeFlow::startPolling);
- connect(&m_tokenPollingTimer, &QTimer::timeout, this,
- &VsDeviceCodeFlow::onPollingTimerTick);
- connect(&m_deviceFlowExpirationTimer, &QTimer::timeout, this,
- &VsDeviceCodeFlow::onDeviceCodeExpired);
-
- m_tokenPollingTimer.setSingleShot(false);
- m_deviceFlowExpirationTimer.setSingleShot(true);
-}
-
-void VsDeviceCodeFlow::grant()
-{
- initDeviceAuthorizationCodeFlow();
-}
-
-void VsDeviceCodeFlow::initDeviceAuthorizationCodeFlow()
-{
- // form initial request for device authorization code
- QNetworkRequest request = QNetworkRequest(
- QUrl(QString("https://%1/oauth/device/code").arg(m_authorizationServerHost)));
-
- request.setRawHeader(QByteArray("Content-Type"),
- QByteArray("application/x-www-form-urlencoded"));
-
- QString data =
- QString("client_id=%1&scope=%2&audience=%3")
- .arg(m_clientId,
- QLatin1String("openid profile email offline_access read:servers"),
- m_audience);
-
- // send request
- QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- bool success = processDeviceCodeNetworkReply(reply);
- if (success) {
- // notify success along with user code and verification URL
- emit deviceCodeFlowInitialized(m_userCode, m_verificationUriComplete);
- } else if (m_authenticationError) {
- // notify failure
- emit deviceCodeFlowError();
- }
- reply->deleteLater();
- });
-}
-
-void VsDeviceCodeFlow::startPolling()
-{
- if (m_pollingInterval <= 0 || m_deviceCodeValidityDuration <= 0) {
- std::cout << "Could not start polling. This should not print and indicates a bug."
- << std::endl;
- return;
- }
-
- // poll on a regular interval, up until the expiration of the code
- m_tokenPollingTimer.setInterval(m_pollingInterval * 1000);
- m_deviceFlowExpirationTimer.setInterval(m_deviceCodeValidityDuration * 1000);
-
- m_tokenPollingTimer.start();
- m_deviceFlowExpirationTimer.start();
-}
-
-void VsDeviceCodeFlow::stopPolling()
-{
- if (m_tokenPollingTimer.isActive()) {
- m_tokenPollingTimer.stop();
- }
- if (m_deviceFlowExpirationTimer.isActive()) {
- m_deviceFlowExpirationTimer.stop();
- }
-}
-
-void VsDeviceCodeFlow::onPollingTimerTick()
-{
- // form request to /oauth/token
- QNetworkRequest request = QNetworkRequest(
- QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
-
- request.setRawHeader(QByteArray("Content-Type"),
- QByteArray("application/x-www-form-urlencoded"));
-
- QString data =
- QString("client_id=%1&device_code=%2&grant_type=%3")
- .arg(m_clientId, m_deviceCode,
- QLatin1String("urn:ietf:params:oauth:grant-type:device_code"));
-
- // send send request for token
- QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- bool success = processPollingOAuthTokenNetworkReply(reply);
- if (m_authenticationError) {
- // shouldn't happen
- emit deviceCodeFlowError();
- } else if (success) {
- // flow successfully completed
- emit onCompletedCodeFlow(m_accessToken, m_refreshToken);
- // cleanup
- stopPolling();
- cleanupDeviceCodeFlow();
- }
- reply->deleteLater();
- });
-}
-
-void VsDeviceCodeFlow::onDeviceCodeExpired()
-{
- emit deviceCodeFlowTimedOut();
-
- std::cout << "Device Code has expired." << std::endl;
- stopPolling();
- cleanupDeviceCodeFlow();
-}
-
-void VsDeviceCodeFlow::cancelCodeFlow()
-{
- stopPolling();
- cleanupDeviceCodeFlow();
-}
-
-bool VsDeviceCodeFlow::processDeviceCodeNetworkReply(QNetworkReply* reply)
-{
- QByteArray buffer = reply->readAll();
-
- // Error: failed to get device code
- if (reply->error()) {
- std::cout << "Failed to get device code: " << buffer.toStdString() << std::endl;
- m_authenticationError = true;
- return false;
- }
-
- // parse JSON from string response
- QJsonParseError parseError;
- QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
- if (parseError.error) {
- std::cout << "Error parsing JSON for Device Code: "
- << parseError.errorString().toStdString() << std::endl;
- m_authenticationError = true;
- return false;
- }
-
- // get fields
- QJsonObject object = data.object();
- m_deviceCode = object.value(QLatin1String("device_code")).toString();
- m_userCode = object.value(QLatin1String("user_code")).toString();
- m_verificationUri = object.value(QLatin1String("verification_uri")).toString();
- m_verificationUriComplete =
- object.value(QLatin1String("verification_uri_complete")).toString();
- m_pollingInterval =
- object.value(QLatin1String("interval")).toInt(2); // default to 2s
- m_deviceCodeValidityDuration =
- object.value(QLatin1String("expires_in")).toInt(900); // default to 900s
-
- // return true if success
- return true;
-}
-
-bool VsDeviceCodeFlow::processPollingOAuthTokenNetworkReply(QNetworkReply* reply)
-{
- QByteArray buffer = reply->readAll();
-
- // Error: failed to get device code (this is expected)
- if (reply->error()) {
- return false;
- }
-
- // parse JSON from string response
- QJsonParseError parseError;
- QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
- if (parseError.error) {
- std::cout << "Error parsing JSON for access token: "
- << parseError.errorString().toStdString() << std::endl;
- return false;
- }
-
- // get fields
- QJsonObject object = data.object();
- m_idToken = object.value(QLatin1String("id_token")).toString();
- m_accessToken = object.value(QLatin1String("access_token")).toString();
- m_refreshToken = object.value(QLatin1String("refresh_token")).toString();
- m_authenticationError = false;
-
- // return true if success
- return true;
-}
-
-void VsDeviceCodeFlow::cleanupDeviceCodeFlow()
-{
- m_deviceCode = QStringLiteral("");
- m_userCode = QStringLiteral("");
- m_verificationUri = QStringLiteral("https://auth.jacktrip.org/activate");
- m_verificationUriComplete = QStringLiteral("");
-
- m_pollingInterval = -1;
- m_deviceCodeValidityDuration = -1;
-}
-
-QString VsDeviceCodeFlow::accessToken()
-{
- return m_accessToken;
-}
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsDeviceCodeFlow.h
- * \author Dominick Hing
- * \date May 2023
- */
-
-#ifndef VSDEVICECODEFLOW_H
-#define VSDEVICECODEFLOW_H
-
-#include <QEventLoop>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonParseError>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QSettings>
-#include <QString>
-#include <QTimer>
-#include <iostream>
-
-#include "vsDeviceCodeFlow.h"
-
-class VsDeviceCodeFlow : public QObject
-{
- Q_OBJECT
-
- public:
- explicit VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager);
- virtual ~VsDeviceCodeFlow() { stopPolling(); }
-
- void grant();
- void refreshAccessToken(){};
- void initDeviceAuthorizationCodeFlow();
-
- bool processDeviceCodeNetworkReply(QNetworkReply* reply);
- bool processPollingOAuthTokenNetworkReply(QNetworkReply* reply);
- void startPolling();
- void stopPolling();
- void onPollingTimerTick();
- void onDeviceCodeExpired();
- void cancelCodeFlow();
- void cleanupDeviceCodeFlow();
-
- bool authenticated();
- QString accessToken();
-
- signals:
- void deviceCodeFlowInitialized(QString code, QString verificationUrl);
- void deviceCodeFlowError();
- void deviceCodeFlowTimedOut();
- void onCompletedCodeFlow(QString accessToken, QString refreshToken);
-
- private:
- QString m_clientId;
- QString m_audience;
- QString m_authorizationServerHost;
-
- // state used specifically in the device code flow
- QString m_deviceCode;
- QString m_userCode;
- QString m_verificationUri;
- QString m_verificationUriComplete;
- int m_pollingInterval = -1; // seconds
- int m_deviceCodeValidityDuration = -1; // seconds
-
- QTimer m_tokenPollingTimer;
- QTimer m_deviceFlowExpirationTimer;
-
- // authentication state variables
- bool m_authenticationError;
- QString m_refreshToken;
- QString m_accessToken;
- QString m_idToken;
-
- QScopedPointer<QNetworkAccessManager> m_netManager;
-};
-
-#endif // VSDEVICECODEFLOW
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsMacPermissions.h
- * \author Matt Horton
- * \date Oct 2022
- */
-
-#ifndef __VSMACPERMISSIONS_H__
-#define __VSMACPERMISSIONS_H__
-
-#include <objc/objc.h>
-
-#include <QDebug>
-#include <QObject>
-#include <QString>
-
-#include "vsPermissions.h"
-
-class VsMacPermissions : public VsPermissions
-{
- Q_OBJECT
-
- public:
- explicit VsMacPermissions();
-
- bool micPermissionChecked() override;
- Q_INVOKABLE void getMicPermission() override;
- Q_INVOKABLE void openSystemPrivacy();
-
- private:
- QString m_micPermission = "unknown";
- bool m_micPermissionChecked = false;
-};
-
-#endif // __VSMACPERMISSIONS_H__
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsMacPermissions.mm
- * \author Matt Horton
- * \date Oct 2022
- */
-
-#include "vsMacPermissions.h"
-#include <Foundation/Foundation.h>
-#include <AVFoundation/AVFoundation.h>
-#include <QDesktopServices>
-#include <QSettings>
-#include <QUrl>
-
-VsMacPermissions::VsMacPermissions()
-{
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- m_micPermissionChecked = settings.value(QStringLiteral("MicPermissionChecked"), false).toBool();
- settings.endGroup();
-}
-
-bool VsMacPermissions::micPermissionChecked()
-{
- if (m_micPermissionChecked) {
- getMicPermission();
- }
- return m_micPermissionChecked;
-}
-
-void VsMacPermissions::getMicPermission()
-{
- if (@available(macOS 10.14, *)) {
- // Request permission to access.
- switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio])
- {
- case AVAuthorizationStatusAuthorized:
- {
- // The user has previously granted access.
- setMicPermission(QStringLiteral("granted"));
- break;
- }
- case AVAuthorizationStatusNotDetermined:
- {
- // The app hasn't yet asked the user for access.
- [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
- if (granted) {
- setMicPermission(QStringLiteral("granted"));
- } else {
- setMicPermission(QStringLiteral("denied"));
- }
- }];
- setMicPermission(QStringLiteral("unknown"));
- break;
- }
- case AVAuthorizationStatusDenied:
- {
- // The user has previously denied access.
- setMicPermission(QStringLiteral("denied"));
- }
- case AVAuthorizationStatusRestricted:
- {
- // The user can't grant access due to restrictions.
- setMicPermission(QStringLiteral("denied"));
- }
- }
- } else {
- setMicPermission(QStringLiteral("granted"));
- }
-}
-
-void VsMacPermissions::openSystemPrivacy()
-{
- QDesktopServices::openUrl(QUrl("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"));
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPermissions.mm
- * \author Matt Horton
- * \date Oct 2022
- */
-
-#include "vsPermissions.h"
-
-#include <QDesktopServices>
-#include <QSettings>
-#include <QUrl>
-
-QString VsPermissions::micPermission()
-{
- return m_micPermission;
-}
-
-bool VsPermissions::micPermissionChecked()
-{
- return m_micPermissionChecked;
-}
-
-void VsPermissions::getMicPermission()
-{
- setMicPermission("granted");
-}
-
-void VsPermissions::setMicPermission(QString status)
-{
- m_micPermission = status;
- m_micPermissionChecked = true;
- emit micPermissionUpdated();
-
- QSettings settings;
- settings.beginGroup(QStringLiteral("VirtualStudio"));
- settings.setValue(QStringLiteral("MicPermissionChecked"), m_micPermissionChecked);
- settings.endGroup();
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPermissions.h
- * \author Matt Horton
- * \date Nov 2022
- */
-
-#ifndef __VSPERMISSIONS_H__
-#define __VSPERMISSIONS_H__
-
-#include <QDebug>
-#include <QObject>
-#include <QString>
-
-class VsPermissions : public QObject
-{
- Q_OBJECT
- Q_PROPERTY(QString micPermission READ micPermission NOTIFY micPermissionUpdated)
-
- public:
- VsPermissions() = default; // define here and there
-
- QString micPermission(); // define here
- virtual bool micPermissionChecked(); // define here and there
- Q_INVOKABLE virtual void getMicPermission();
- void setMicPermission(QString status); // define here
-
- signals:
- void micPermissionUpdated(); // leave here
-
- protected:
-#if __APPLE__
- QString m_micPermission = "unknown";
- bool m_micPermissionChecked = false;
-#else
- QString m_micPermission = "granted";
- bool m_micPermissionChecked = true;
-#endif
-};
-
-#endif // __VSPERMISSIONS_H__
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPinger.cpp
- * \author Dominick Hing
- * \date July 2022
- */
-
-#include "vsPing.h"
-
-#include <iostream>
-
-using std::cout;
-using std::endl;
-
-// NOTE: It's better not to use
-// using namespace std;
-// because some functions (like exit()) get confused with QT functions
-
-//*******************************************************************************
-VsPing::VsPing(uint32_t pingNum, uint32_t timeout_msec) : mPingNumber(pingNum)
-{
- connect(&mTimer, &QTimer::timeout, this, &VsPing::onTimeout);
-
- mTimer.setTimerType(Qt::PreciseTimer);
- mTimer.setSingleShot(true);
- mTimer.setInterval(timeout_msec);
- mTimer.start();
-}
-
-void VsPing::send()
-{
- QDateTime now = QDateTime::currentDateTime();
- mSent = now;
-}
-
-void VsPing::receive()
-{
- QDateTime now = QDateTime::currentDateTime();
- if (!mTimedOut) {
- mTimer.stop();
- mReceivedReply = true;
- mReceived = now;
- }
-}
-
-void VsPing::onTimeout()
-{
- if (!mReceivedReply) {
- mTimedOut = true;
- emit timeout(mPingNumber);
- }
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPing.h
- * \author Dominick Hing
- * \date July 2022
- */
-
-#ifndef VSPING_H
-#define VSPING_H
-
-#include <QAbstractSocket>
-#include <QDateTime>
-#include <QObject>
-#include <QTimer>
-#include <QtWebSockets>
-#include <stdexcept>
-
-/** \brief A helper class for VsPinger
- *
- */
-class VsPing : public QObject
-{
- Q_OBJECT;
-
- public:
- explicit VsPing(uint32_t pingNum, uint32_t timeout_msec);
- virtual ~VsPing() { mTimer.stop(); }
- uint32_t pingNumber() { return mPingNumber; }
-
- QDateTime sentTimestamp() { return mSent; }
- QDateTime receivedTimestamp() { return mReceived; }
- bool receivedReply() { return mReceivedReply; }
- bool timedOut() { return mTimedOut; }
-
- void send();
- void receive();
-
- private:
- uint32_t mPingNumber;
- QDateTime mSent;
- QDateTime mReceived;
-
- QTimer mTimer;
- bool mTimedOut = false;
- bool mReceivedReply = false;
-
- public slots:
- void onTimeout();
-
- signals:
- void timeout(uint32_t pingNum);
-};
-
-#endif // VSPING_H
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPinger.cpp
- * \author Dominick Hing
- * \date July 2022
- */
-
-#include "vsPinger.h"
-
-#include <iostream>
-
-using std::cout;
-using std::endl;
-
-// NOTE: It's better not to use
-// using namespace std;
-// because some functions (like exit()) get confused with QT functions
-
-//*******************************************************************************
-VsPinger::VsPinger(QString scheme, QString host, QString path)
-{
- mURL.setScheme(scheme);
- mURL.setHost(host);
- mURL.setPath(path);
-
- mTimer.setTimerType(Qt::PreciseTimer);
-
- connect(&mSocket, &QWebSocket::binaryMessageReceived, this,
- &VsPinger::onReceivePingMessage);
- connect(&mSocket, &QWebSocket::connected, this, &VsPinger::onConnected);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
- connect(&mSocket,
- QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
- &VsPinger::onError);
-#else
- connect(&mSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
- this, &VsPinger::onError);
-#endif
- connect(&mTimer, &QTimer::timeout, this, &VsPinger::onPingTimer);
-}
-
-//*******************************************************************************
-void VsPinger::start()
-{
- // fail to start if no token is supplied
- if (mToken.toStdString() == "") {
- std::cout << "Error: auth token is not set" << std::endl;
- return;
- }
-
- mTimer.setInterval(mPingInterval);
-
- QString authVal = "Bearer ";
- authVal.append(mToken);
-
- QNetworkRequest req = QNetworkRequest(QUrl(mURL));
- req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
- req.setRawHeader(QByteArray("Connection"), QByteArray("upgrade"));
- req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
- mSocket.open(req);
-
- mStarted = true;
-}
-
-//*******************************************************************************
-void VsPinger::stop()
-{
- mStarted = false;
- mError = false;
- mTimer.stop();
- mSocket.close(QWebSocketProtocol::CloseCodeNormal, NULL);
-}
-
-//*******************************************************************************
-void VsPinger::setToken(QString token)
-{
- if (mStarted) {
- std::cout << "Error: cannot set token while pinger is active." << std::endl;
- return;
- }
-
- mToken = token;
- mAuthorized = true;
-};
-
-//*******************************************************************************
-void VsPinger::unsetToken()
-{
- if (mStarted) {
- std::cout << "Error: cannot unset token while pinger is active." << std::endl;
- return;
- }
-
- mToken = QString();
- mAuthorized = false;
-}
-
-//*******************************************************************************
-void VsPinger::sendPingMessage(const QByteArray& message)
-{
- if (mAuthorized && !mError) {
- mSocket.sendBinaryMessage(message);
- }
-}
-
-//*******************************************************************************
-void VsPinger::updateStats()
-{
- PingStat stat;
- stat.packetsReceived = 0;
- stat.packetsSent = 0;
-
- uint32_t count = 0;
-
- std::vector<uint32_t> vec_expired;
- std::vector<qint64> vec_rtt;
- std::map<uint32_t, VsPing*>::reverse_iterator it;
- for (it = mPings.rbegin(); it != mPings.rend(); ++it) {
- VsPing* ping = it->second;
-
- // mark this ping as ready to delete, since it will no longer be used in stats
- if (count >= mPingNumPerInterval) {
- vec_expired.push_back(ping->pingNumber());
- count++;
- } else if (ping->timedOut() || ping->receivedReply()) {
- // Only include in statistics pings that have timed out or been received.
- // All others are pending and are not considered in statistics
- stat.packetsSent++;
- if (ping->receivedReply()) {
- stat.packetsReceived++;
- }
-
- QDateTime sent = ping->sentTimestamp();
- QDateTime received = ping->receivedTimestamp();
- qint64 diff = sent.msecsTo(received);
-
- // don't include case where dif = 0 in stats, mark as expired instead
- if (diff != 0) {
- vec_rtt.push_back(diff);
- } else {
- vec_expired.push_back(ping->pingNumber());
- }
-
- count++;
- }
- }
-
- // Deleted pings marked as expired by freeing the Ping object
- // and clearing the map item
- for (std::vector<uint32_t>::iterator it_expired = vec_expired.begin();
- it_expired != vec_expired.end(); it_expired++) {
- uint32_t expiredPingNum = *it_expired;
- delete mPings.at(expiredPingNum);
- mPings.erase(expiredPingNum);
- }
-
- // Update RTT stats
- double min_rtt = 0.0;
- double max_rtt = 0.0;
- double avg_rtt = 0.0;
- double stddev_rtt = 0.0;
-
- // avoid edge case due to min_rtt and max_rtt being at the numeric limits
- // when vector size is 0
- if (vec_rtt.size() == 0) {
- stat.maxRtt = 0;
- stat.minRtt = 0;
- stat.avgRtt = 0;
- stat.stdDevRtt = 0;
-
- // Update mStats
- mStats = stat;
- return;
- }
-
- for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
- it_rtt++) {
- double rtt = (double)*it_rtt;
- if (rtt < min_rtt || min_rtt == 0.0) {
- min_rtt = rtt;
- }
- if (rtt > max_rtt || max_rtt == 0.0) {
- max_rtt = rtt;
- }
-
- avg_rtt += rtt / vec_rtt.size();
- }
-
- for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
- it_rtt++) {
- double rtt = (double)*it_rtt;
- stddev_rtt += (rtt - avg_rtt) * (rtt - avg_rtt);
- }
- stddev_rtt /= vec_rtt.size();
- stddev_rtt = sqrt(stddev_rtt);
-
- stat.maxRtt = max_rtt;
- stat.minRtt = min_rtt;
- stat.avgRtt = avg_rtt;
- stat.stdDevRtt = stddev_rtt;
-
- // Update mStats
- mStats = stat;
- return;
-}
-
-//*******************************************************************************
-VsPinger::PingStat VsPinger::getPingStats()
-{
- return mStats;
-}
-
-//*******************************************************************************
-void VsPinger::onError(QAbstractSocket::SocketError error)
-{
- cout << "WebSocket Error: " << error << endl;
- mError = true;
- mStarted = false;
- mTimer.stop();
-}
-
-//*******************************************************************************
-void VsPinger::onConnected()
-{
- // start the ping timer after the connection is established
- mTimer.start();
-}
-
-//*******************************************************************************
-void VsPinger::onPingTimer()
-{
- updateStats();
-
- QByteArray bytes = QByteArray::number(mPingCount);
- QDateTime now = QDateTime::currentDateTime();
- this->sendPingMessage(bytes);
-
- VsPing* ping = new VsPing(mPingCount, mPingInterval);
- ping->send();
- mPings[mPingCount] = ping;
-
- connect(ping, &VsPing::timeout, this, &VsPinger::onPingTimeout);
-
- mLastPacketSent = mPingCount;
- mPingCount++;
-}
-
-//*******************************************************************************
-void VsPinger::onPingTimeout(uint32_t pingNum)
-{
- std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
- if (it == mPings.end()) {
- return;
- }
-
- updateStats();
-}
-
-//*******************************************************************************
-void VsPinger::onReceivePingMessage(const QByteArray& message)
-{
- QDateTime now = QDateTime::currentDateTime();
- uint32_t pingNum = message.toUInt();
-
- // locate the appropriate corresponding ping message
- std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
- if (it == mPings.end()) {
- return;
- }
-
- VsPing* ping = (*it).second;
-
- // do not apply to pings that have timed out
- if (!ping->timedOut()) {
- // update ping data
- ping->receive();
-
- // update vsPinger
- mHasReceivedPing = true;
- mLastPacketReceived = pingNum;
- if (pingNum > mLargestPingNumReceived) {
- mLargestPingNumReceived = pingNum;
- }
- }
-
- updateStats();
-}
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2021 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 vsPinger.h
- * \author Dominick Hing
- * \date July 2022
- */
-
-#ifndef VSPINGER_H
-#define VSPINGER_H
-
-#include <QAbstractSocket>
-#include <QDateTime>
-#include <QObject>
-#include <QTimer>
-#include <QUrl>
-#include <QtWebSockets>
-#include <stdexcept>
-#include <vector>
-
-#include "vsPing.h"
-
-/** \brief VsPinger for generating latency statistics between
- * Virtual Studio devices and Virtual Studio Servers
- *
- */
-class VsPinger : public QObject
-{
- Q_OBJECT;
-
- public:
- /** \brief The class constructor
- * \param scheme The protocol scheme for the pinger
- * \param host The hostname of the server
- * \param path The path to ping the server on
- */
- explicit VsPinger(QString scheme, QString host, QString path);
- virtual ~VsPinger() { stop(); }
- void start();
- void stop();
- bool active() { return mStarted; };
- void setToken(QString token);
- void unsetToken();
-
- struct PingStat {
- uint32_t packetsReceived = 0;
- uint32_t packetsSent = 0;
- double minRtt = 0.0;
- double maxRtt = 0.0;
- double avgRtt = 0.0;
- double stdDevRtt = 0.0;
- };
-
- PingStat getPingStats();
-
- private:
- QWebSocket mSocket;
- QUrl mURL;
- QString mToken;
- bool mAuthorized = false;
- bool mStarted = false;
- bool mError = false;
-
- QTimer mTimer;
- uint32_t mPingCount = 0;
- const uint32_t mPingNumPerInterval = 5;
- const uint32_t mPingInterval = 1000;
- const uint32_t mPingTimeout = 1000;
-
- std::map<uint32_t, VsPing*> mPings;
-
- uint32_t mLastPacketSent;
- uint32_t mLastPacketReceived;
- uint32_t mLargestPingNumReceived =
- 0; // is 0 if no ping has been received, otherwise, is the largest ping number
- // received
- bool mHasReceivedPing = false; // used for edge case where we have't received a ping
- // yet (mLargestPingNumReceived = 0)
-
- PingStat mStats;
-
- void sendPingMessage(const QByteArray& message);
- void updateStats();
-
- private slots:
- void onError(QAbstractSocket::SocketError error);
- void onConnected();
- void onPingTimer();
- void onPingTimeout(uint32_t pingNum);
- void onReceivePingMessage(const QByteArray& message);
-};
-
-#endif // VSPINGER_H
\ No newline at end of file
+++ /dev/null
-#ifndef VSQMLCLIPBOARD_H
-#define VSQMLCLIPBOARD_H
-
-#include <QApplication>
-#include <QClipboard>
-#include <QObject>
-
-class VsQmlClipboard : public QObject
-{
- Q_OBJECT
- public:
- explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent)
- {
- clipboard = QApplication::clipboard();
- }
-
- Q_INVOKABLE void setText(QString text)
- {
- clipboard->setText(text, QClipboard::Clipboard);
- }
-
- private:
- QClipboard* clipboard;
-};
-
-#endif // VSQMLCLIPBOARD_H
\ No newline at end of file
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsQuickView.cpp
- * \author Aaron Wyatt
- * \date March 2022
- */
-
-#include "vsQuickView.h"
-
-#include <QDesktopServices>
-#include <iostream>
-
-VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent)
-{
-#ifdef Q_OS_MACOS
- auto* quit = new QAction("&Quit", this);
-
- QMenuBar* menuBar = new QMenuBar(nullptr);
- QMenu* appName = menuBar->addMenu("&JackTrip");
- appName->addAction(quit);
-
- connect(quit, &QAction::triggered, this, &VsQuickView::closeWindow);
-#endif
-}
-
-bool VsQuickView::event(QEvent* event)
-{
- if (event->type() == QEvent::Close || event->type() == QEvent::Quit) {
- emit windowClose();
- event->ignore();
- }
- return QQuickView::event(event);
-}
-
-void VsQuickView::closeWindow()
-{
- emit windowClose();
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsQuickView.h
- * \author Aaron Wyatt
- * \date March 2022
- */
-
-#ifndef VSQUICKVIEW_H
-#define VSQUICKVIEW_H
-
-#include <QQuickView>
-#ifdef Q_OS_MACOS
-#include <QAction>
-#include <QMenu>
-#include <QMenuBar>
-#include <QObject>
-#endif
-
-class VsQuickView : public QQuickView
-{
- Q_OBJECT
-
- public:
- VsQuickView(QWindow* parent = nullptr);
- bool event(QEvent* event) override;
-
- signals:
- void windowClose();
-
- private slots:
- void closeWindow();
-};
-
-#endif // VSQUICKVIEW_H
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsServerInfo.cpp
- * \author Aaron Wyatt
- * \date March 2022
- */
-
-#include "vsServerInfo.h"
-
-VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {}
-
-VsServerInfo& VsServerInfo::operator=(const VsServerInfo& info)
-{
- m_section = info.m_section;
- m_name = info.m_name;
- m_host = info.m_host;
- m_port = info.m_port;
- m_enabled = info.m_enabled;
- m_owner = info.m_owner;
- m_admin = info.m_admin;
- m_isManaged = info.m_isManaged;
- m_isPublic = info.m_isPublic;
- m_region = info.m_region;
- m_period = info.m_period;
- m_sampleRate = info.m_sampleRate;
- m_queueBuffer = info.m_queueBuffer;
- m_bannerURL = info.m_bannerURL;
- m_id = info.m_id;
- m_sessionId = info.m_sessionId;
- m_streamId = info.m_streamId;
- m_status = info.m_status;
- m_cloudId = info.m_cloudId;
- m_inviteKey = info.m_inviteKey;
- return *this;
-}
-
-VsServerInfo::serverSectionT VsServerInfo::section()
-{
- return m_section;
-}
-
-QString VsServerInfo::type() const
-{
- if (m_section == YOUR_STUDIOS) {
- return QStringLiteral("Your Studios");
- } else if (m_section == SUBSCRIBED_STUDIOS) {
- return QStringLiteral("Subscribed Studios");
- } else {
- return QStringLiteral("Public Studios");
- }
-}
-
-void VsServerInfo::setSection(serverSectionT section)
-{
- m_section = section;
-}
-
-QString VsServerInfo::name() const
-{
- return m_name;
-}
-
-void VsServerInfo::setName(const QString& name)
-{
- m_name = name;
-}
-
-QString VsServerInfo::host() const
-{
- return m_host;
-}
-
-QString VsServerInfo::status() const
-{
- return m_status;
-}
-
-bool VsServerInfo::canConnect() const
-{
- return !m_host.isEmpty() && m_status == "Ready";
-}
-
-bool VsServerInfo::canStart() const
-{
- return m_owner || m_admin;
-}
-
-void VsServerInfo::setHost(const QString& host)
-{
- m_host = host;
- emit canConnectChanged();
-}
-
-void VsServerInfo::setStatus(const QString& status)
-{
- m_status = status;
- emit canConnectChanged();
-}
-
-quint16 VsServerInfo::port() const
-{
- return m_port;
-}
-
-void VsServerInfo::setPort(quint16 port)
-{
- m_port = port;
-}
-
-bool VsServerInfo::enabled() const
-{
- return m_enabled;
-}
-
-void VsServerInfo::setEnabled(bool enabled)
-{
- m_enabled = enabled;
-}
-
-bool VsServerInfo::isOwner() const
-{
- return m_owner;
-}
-
-void VsServerInfo::setIsOwner(bool owner)
-{
- m_owner = owner;
-}
-
-bool VsServerInfo::isAdmin() const
-{
- return m_admin;
-}
-
-void VsServerInfo::setIsAdmin(bool admin)
-{
- m_admin = admin;
-}
-
-bool VsServerInfo::isPublic() const
-{
- return m_isPublic;
-}
-
-void VsServerInfo::setIsPublic(bool isPublic)
-{
- m_isPublic = isPublic;
-}
-
-QString VsServerInfo::region() const
-{
- return m_region;
-}
-
-QString VsServerInfo::flag() const
-{
- QStringList parts = m_region.split(QStringLiteral("-"));
- if (parts.count() > 1) {
- QString countryCode = parts.at(1).toUpper();
- if (countryCode == QStringLiteral("TF")) {
- countryCode = QStringLiteral("TW");
- }
- return QStringLiteral("flags/%1.svg").arg(countryCode);
- }
- // Have a fallback here
- return QStringLiteral("flags/US.svg");
-}
-
-QString VsServerInfo::location() const
-{
- return m_region;
-}
-
-void VsServerInfo::setRegion(const QString& region)
-{
- m_region = region;
-}
-
-bool VsServerInfo::isManaged() const
-{
- return m_isManaged;
-}
-
-void VsServerInfo::setIsManaged(bool isManaged)
-{
- m_isManaged = isManaged;
-}
-
-quint16 VsServerInfo::period() const
-{
- return m_period;
-}
-
-void VsServerInfo::setPeriod(quint16 period)
-{
- m_period = period;
-}
-
-quint32 VsServerInfo::sampleRate() const
-{
- return m_sampleRate;
-}
-
-void VsServerInfo::setSampleRate(quint32 sampleRate)
-{
- m_sampleRate = sampleRate;
-}
-
-quint16 VsServerInfo::queueBuffer() const
-{
- return m_queueBuffer;
-}
-
-void VsServerInfo::setQueueBuffer(quint16 queueBuffer)
-{
- m_queueBuffer = queueBuffer;
-}
-
-QString VsServerInfo::bannerURL() const
-{
- return m_bannerURL;
-}
-
-void VsServerInfo::setBannerURL(const QString& bannerURL)
-{
- m_bannerURL = bannerURL;
-}
-
-QString VsServerInfo::id() const
-{
- return m_id;
-}
-
-void VsServerInfo::setId(const QString& id)
-{
- m_id = id;
-}
-
-QString VsServerInfo::sessionId() const
-{
- return m_sessionId;
-}
-
-void VsServerInfo::setSessionId(const QString& sessionId)
-{
- m_sessionId = (sessionId == "undefined") ? "" : sessionId;
-}
-
-QString VsServerInfo::streamId() const
-{
- return m_streamId;
-}
-
-void VsServerInfo::setStreamId(const QString& streamId)
-{
- m_streamId = (streamId == "undefined") ? "" : streamId;
-}
-
-QString VsServerInfo::inviteKey() const
-{
- return m_inviteKey;
-}
-
-void VsServerInfo::setInviteKey(const QString& inviteKey)
-{
- m_inviteKey = inviteKey;
-}
-
-QString VsServerInfo::cloudId() const
-{
- return m_cloudId;
-}
-
-void VsServerInfo::setCloudId(const QString& cloudId)
-{
- m_cloudId = cloudId;
-}
-
-bool VsServerInfo::operator<(const VsServerInfo& other) const
-{
- if (status() == QStringLiteral("Ready")) {
- if (other.status() != QStringLiteral("Ready")) {
- return true;
- }
- } else if (other.status() == QStringLiteral("Ready")) {
- return false;
- }
- return name() < other.name();
-}
-
-VsServerInfo::~VsServerInfo() = default;
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsServerInfo.h
- * \author Aaron Wyatt
- * \date March 2022
- */
-
-#ifndef VSSERVERINFO_H
-#define VSSERVERINFO_H
-
-#include <QObject>
-
-class VsServerInfo : public QObject
-{
- Q_OBJECT
-
- Q_PROPERTY(QString type READ type CONSTANT)
- Q_PROPERTY(QString name READ name CONSTANT)
- // Q_PROPERTY(QString host READ host CONSTANT)
- Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged)
- Q_PROPERTY(bool canStart READ canStart CONSTANT)
- // Q_PROPERTY(quint16 port READ port CONSTANT)
- Q_PROPERTY(bool isPublic READ isPublic CONSTANT)
- Q_PROPERTY(QString flag READ flag CONSTANT)
- Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT)
- Q_PROPERTY(QString location READ location CONSTANT)
- Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT)
- Q_PROPERTY(bool isManaged READ isManaged CONSTANT)
- Q_PROPERTY(quint16 period READ period CONSTANT)
- Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT)
- Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT)
- Q_PROPERTY(QString sessionId READ sessionId CONSTANT)
- Q_PROPERTY(QString streamId READ streamId CONSTANT)
- Q_PROPERTY(QString status READ status CONSTANT)
- Q_PROPERTY(bool enabled READ enabled CONSTANT)
- Q_PROPERTY(QString cloudId READ cloudId CONSTANT)
- Q_PROPERTY(QString id READ id CONSTANT)
- Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT)
-
- public:
- enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS };
-
- explicit VsServerInfo(QObject* parent = nullptr);
- VsServerInfo& operator=(const VsServerInfo& info);
- ~VsServerInfo() override;
-
- serverSectionT section();
- QString type() const;
- void setSection(serverSectionT section);
- QString name() const;
- void setName(const QString& name);
- QString host() const;
- bool canConnect() const;
- bool canStart() const;
- void setHost(const QString& host);
- quint16 port() const;
- void setPort(quint16 port);
- bool enabled() const;
- void setEnabled(bool enabled);
- bool isOwner() const;
- void setIsOwner(bool owner);
- bool isAdmin() const;
- void setIsAdmin(bool admin);
- bool isPublic() const;
- void setIsPublic(bool isPublic);
- QString region() const;
- QString flag() const;
- QString location() const;
- void setRegion(const QString& region);
- bool isManaged() const;
- void setIsManaged(bool isManageable);
- quint16 period() const;
- void setPeriod(quint16 period);
- quint32 sampleRate() const;
- void setSampleRate(quint32 sampleRate);
- quint16 queueBuffer() const;
- void setQueueBuffer(quint16 queueBuffer);
- QString bannerURL() const;
- void setBannerURL(const QString& bannerURL);
- QString id() const;
- void setId(const QString& id);
- QString sessionId() const;
- void setSessionId(const QString& sessionId);
- QString streamId() const;
- void setStreamId(const QString& streamId);
- QString status() const;
- void setStatus(const QString& status);
- QString inviteKey() const;
- void setInviteKey(const QString& inviteKey);
- QString cloudId() const;
- void setCloudId(const QString& cloudId);
- bool operator<(const VsServerInfo& other) const;
-
- signals:
- void canConnectChanged();
-
- private:
- serverSectionT m_section = PUBLIC_STUDIOS;
- QString m_name;
- QString m_host;
- quint16 m_port;
- bool m_enabled;
- bool m_owner;
- bool m_admin;
- bool m_isManaged;
- bool m_isPublic;
- QString m_region;
- quint16 m_period;
- quint32 m_sampleRate;
- quint16 m_queueBuffer;
- QString m_bannerURL;
- QString m_id;
- QString m_sessionId;
- QString m_streamId;
- QString m_status;
- QString m_cloudId;
- QString m_inviteKey;
-
- /* Remaining JSON fields
- "loopback": true,
- "stereo": true,
- "type": "JackTrip",
- "size": "c5.large",
- "mixBranch": "main",
- "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;",
- "ownerId": "string",
- "subStatus": "Active",
- "createdAt": "2021-09-07T17:15:38Z",
- "expiresAt": "2021-09-07T17:15:38Z",
- "updatedAt": "2021-09-07T17:15:38Z"
- */
-};
-
-#endif // VSSERVERINFO_H
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsWebSocket.cpp
- * \author Matt Horton
- * \date June 2022
- */
-
-#include "vsWebSocket.h"
-
-#include <QDebug>
-#include <iostream>
-
-// Constructor
-VsWebSocket::VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
- QString apiSecret, QObject* parent)
- : QObject(parent)
- , m_url(url)
- , m_token(token)
- , m_apiPrefix(apiPrefix)
- , m_apiSecret(apiSecret)
-{
- m_webSocket.reset(new QWebSocket());
- connect(m_webSocket.get(), &QWebSocket::disconnected, this,
- &VsWebSocket::disconnected);
- connect(m_webSocket.get(),
- QOverload<const QList<QSslError>&>::of(&QWebSocket::sslErrors), this,
- &VsWebSocket::onSslErrors);
-#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
- connect(m_webSocket.get(),
- QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
- &VsWebSocket::onError);
-#else
- connect(m_webSocket.get(),
- QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this,
- &VsWebSocket::onError);
-#endif
- connect(m_webSocket.get(), &QWebSocket::textMessageReceived, this,
- &VsWebSocket::textMessageReceived);
-}
-
-VsWebSocket::~VsWebSocket()
-{
- if (isValid()) {
- closeSocket();
- }
- if (!m_webSocket.isNull()) {
- m_webSocket->disconnect();
- m_webSocket.reset();
- }
-}
-
-void VsWebSocket::openSocket()
-{
- if (isValid()) {
- return;
- }
-
- QNetworkRequest req = QNetworkRequest(QUrl(m_url));
- QString authVal = "Bearer ";
- authVal.append(m_token);
- req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
- req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade"));
- req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
- req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local"));
- req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8());
- req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8());
-
- if (!m_webSocket.isNull()) {
- m_webSocket->open(req);
- qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
- }
-}
-
-void VsWebSocket::closeSocket()
-{
- if (!m_webSocket.isNull()
- && m_webSocket->state() != QAbstractSocket::UnconnectedState) {
- qDebug() << "Closing websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
- m_webSocket->abort();
- }
-}
-
-void VsWebSocket::onError(QAbstractSocket::SocketError error)
-{
- // RemoteHostClosedError may be expected due to finite connection durations
- // ConnectionRefusedError may be expected if the server-side endpoint is closed
- if (error != QAbstractSocket::RemoteHostClosedError) {
- qDebug() << "Websocket error: " << error;
- }
- if (!m_webSocket.isNull()) {
- m_webSocket->abort();
- }
-}
-
-void VsWebSocket::onSslErrors(const QList<QSslError>& errors)
-{
- for (int i = 0; i < errors.size(); ++i) {
- qDebug() << "SSL error: " << errors.at(i);
- }
- if (!m_webSocket.isNull()) {
- m_webSocket->abort();
- }
-}
-
-void VsWebSocket::sendMessage(const QByteArray& message)
-{
- if (isValid()) {
- m_webSocket->sendBinaryMessage(message);
- }
-}
-
-bool VsWebSocket::isValid()
-{
- return !m_webSocket.isNull()
- && m_webSocket->state() == QAbstractSocket::ConnectedState;
-}
+++ /dev/null
-//*****************************************************************
-/*
- JackTrip: A System for High-Quality Audio Network Performance
- over the Internet
-
- Copyright (c) 2008-2022 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 vsWebSocket.h
- * \author Matt Horton
- * \date June 2022
- */
-
-#ifndef VSWEBSOCKET_H
-#define VSWEBSOCKET_H
-
-#include <QList>
-#include <QObject>
-#include <QScopedPointer>
-#include <QSslError>
-#include <QString>
-#include <QUrl>
-#include <QtWebSockets>
-
-class VsWebSocket : public QObject
-{
- Q_OBJECT
-
- public:
- // Constructor
- explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
- QString apiSecret, QObject* parent = nullptr);
- virtual ~VsWebSocket();
-
- // Public functions
- void openSocket();
- void closeSocket();
- void sendMessage(const QByteArray& message);
- bool isValid();
-
- signals:
- void textMessageReceived(const QString& message);
- void disconnected();
-
- private slots:
- void onError(QAbstractSocket::SocketError error);
- void onSslErrors(const QList<QSslError>& errors);
-
- private:
- QScopedPointer<QWebSocket> m_webSocket;
- QUrl m_url;
- QString m_token;
- QString m_apiPrefix;
- QString m_apiSecret;
-};
-
-#endif // VSWEBSOCKET_H
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M92-120q-9 0-15.652-4.125Q69.696-128.25 66-135q-4.167-6.6-4.583-14.3Q61-157 66-165l388-670q5-8 11.5-11.5T480-850q8 0 14.5 3.5T506-835l388 670q5 8 4.583 15.7-.416 7.7-4.583 14.3-3.696 6.75-10.348 10.875Q877-120 868-120H92Zm52-60h672L480-760 144-180Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5Zm0-111q12.825 0 21.325-8.625T514-378v-164q0-12.75-8.675-21.375-8.676-8.625-21.5-8.625-12.825 0-21.325 8.625T454-542v164q0 12.75 8.675 21.375 8.676 8.625 21.5 8.625ZM480-470Z"/></svg>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- width="13.758333mm"
- height="21.960417mm"
- viewBox="0 0 13.758333 21.960417"
- version="1.1"
- id="svg5"
- inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
- sodipodi:docname="wedge.svg"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg">
- <sodipodi:namedview
- id="namedview7"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageshadow="2"
- inkscape:pageopacity="0.0"
- inkscape:pagecheckerboard="0"
- inkscape:document-units="mm"
- showgrid="false"
- fit-margin-top="0"
- fit-margin-left="0"
- fit-margin-right="0"
- fit-margin-bottom="0"
- inkscape:zoom="2.0630341"
- inkscape:cx="-46.291044"
- inkscape:cy="47.26049"
- inkscape:window-width="1920"
- inkscape:window-height="1007"
- inkscape:window-x="0"
- inkscape:window-y="0"
- inkscape:window-maximized="1"
- inkscape:current-layer="layer1" />
- <defs
- id="defs2" />
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1">
- <path
- style="fill:#0c1424;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
- id="wedge"
- sodipodi:nodetypes="ccccc" />
- </g>
-</svg>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- width="13.758333mm"
- height="21.960417mm"
- viewBox="0 0 13.758333 21.960417"
- version="1.1"
- id="svg5"
- inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
- sodipodi:docname="wedge_inactive.svg"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg">
- <sodipodi:namedview
- id="namedview7"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageshadow="2"
- inkscape:pageopacity="0.0"
- inkscape:pagecheckerboard="0"
- inkscape:document-units="mm"
- showgrid="false"
- fit-margin-top="0"
- fit-margin-left="0"
- fit-margin-right="0"
- fit-margin-bottom="0"
- inkscape:zoom="4.0967594"
- inkscape:cx="0.36614306"
- inkscape:cy="41.008022"
- inkscape:window-width="1312"
- inkscape:window-height="856"
- inkscape:window-x="0"
- inkscape:window-y="38"
- inkscape:window-maximized="0"
- inkscape:current-layer="layer1" />
- <defs
- id="defs2" />
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1">
- <path
- style="fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
- id="wedge"
- sodipodi:nodetypes="ccccc" />
- </g>
-</svg>
--- /dev/null
+<RCC>
+ <qresource prefix="images">
+ <file>icon_256.png</file>
+ <file>icon_128.png</file>
+ <file>icon_32.png</file>
+ </qresource>
+</RCC>
#elif defined(_WIN32)
void setRealtimeProcessPriority()
{
- if (SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS) == 0) {
- std::cerr << "Failed to set process priority class." << std::endl;
+ if (SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS) == 0
+ || GetPriorityClass(GetCurrentProcess()) != REALTIME_PRIORITY_CLASS) {
+ std::string priority = "unknown";
+ switch (GetPriorityClass(GetCurrentProcess())) {
+ case ABOVE_NORMAL_PRIORITY_CLASS:
+ priority = "above normal";
+ break;
+ case BELOW_NORMAL_PRIORITY_CLASS:
+ priority = "below normal";
+ break;
+ case HIGH_PRIORITY_CLASS:
+ priority = "high";
+ break;
+ case IDLE_PRIORITY_CLASS:
+ priority = "idle";
+ break;
+ case NORMAL_PRIORITY_CLASS:
+ priority = "high";
+ break;
+ case REALTIME_PRIORITY_CLASS:
+ priority = "realtime";
+ break;
+ }
+ std::cerr << "Failed to set process priority class (priority = " << priority
+ << ")" << std::endl;
+ } else {
+ std::cout << "Set process priority class to realtime" << std::endl;
}
- if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) == 0) {
- std::cerr << "Failed to set thread priority." << std::endl;
+ if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) == 0
+ || GetThreadPriority(GetCurrentThread()) != THREAD_PRIORITY_TIME_CRITICAL) {
+ std::string priority = "unknown";
+ switch (GetThreadPriority(GetCurrentThread())) {
+ case THREAD_MODE_BACKGROUND_BEGIN:
+ priority = "background begin";
+ break;
+ case THREAD_MODE_BACKGROUND_END:
+ priority = "background end";
+ break;
+ case THREAD_PRIORITY_ABOVE_NORMAL:
+ priority = "above normal";
+ break;
+ case THREAD_PRIORITY_BELOW_NORMAL:
+ priority = "below normal";
+ break;
+ case THREAD_PRIORITY_HIGHEST:
+ priority = "highest";
+ break;
+ case THREAD_PRIORITY_IDLE:
+ priority = "idle";
+ break;
+ case THREAD_PRIORITY_LOWEST:
+ priority = "lowest";
+ break;
+ case THREAD_PRIORITY_NORMAL:
+ priority = "normal";
+ break;
+ case THREAD_PRIORITY_TIME_CRITICAL:
+ priority = "time critical";
+ break;
+ }
+ std::cerr << "Failed to set thread priority (priority = " << priority << ")"
+ << std::endl;
+ } else {
+ std::cout << "Set thread priority to time critical" << std::endl;
}
}
#else
#include "AudioInterface.h"
-constexpr const char* const gVersion = "2.3.1"; ///< JackTrip version
+constexpr const char* const gVersion = "2.4.0"; ///< JackTrip version
//*******************************************************************************
/// \name Default Values
constexpr const char* gDefaultLocalAddress = "";
constexpr int gDefaultRedundancy = 1;
constexpr int gTimeOutMultiThreadedServer = 10000; // seconds
-constexpr int gWaitCounter = 60;
-constexpr int gUdpWaitTimeout = 30; // milliseconds
+constexpr int gUdpWaitTimeout = 50; // milliseconds
//@}
//*******************************************************************************
* \date July 2020
*/
-#ifndef NO_GUI
-#include <QApplication>
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
-#include <QQuickStyle>
-#endif
-
-#ifndef NO_UPDATER
-#include "dblsqd/feed.h"
-#include "dblsqd/update_dialog.h"
-#endif
-
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
-#include <QDebug>
-#include <QDir>
-#include <QFile>
-#include <QQmlEngine>
-#include <QQuickView>
-#include <QSGRendererInterface>
-#include <QSettings>
-#include <QStandardPaths>
-#include <QTextStream>
-// TODO: Add support for QtWebView
-// #include <QtWebView>
-#include <QtWebEngineQuick/qtwebenginequickglobal.h>
-
-#include "JTApplication.h"
-#include "gui/virtualstudio.h"
-#include "gui/vsDeeplink.h"
-#include "gui/vsQmlClipboard.h"
-#endif // NO_VS && QT_VERSION
-
-#include "gui/qjacktrip.h"
-#else
#include <QCoreApplication>
-#endif
#include <QLoggingCategory>
#include <QScopedPointer>
#include <csignal>
#include "UdpHubListener.h"
#include "jacktrip_globals.h"
+#ifndef NO_GUI
+#include "UserInterface.h"
+#endif
+
#ifdef _WIN32
#include <psapi.h>
#include <tlhelp32.h>
#include <windows.h>
#endif
-#ifndef NO_GUI
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
-static QTextStream* ts;
-static QFile outFile;
-#endif // NO_VS && QT_VERSION
-#endif // NO_GUI
-
QCoreApplication* createApplication(int& argc, char* argv[])
{
// Check for some specific, GUI related command line options.
bool forceGui = false;
+ bool testGui = false;
for (int i = 1; i < argc; i++) {
if (strncmp(argv[i], "--gui", 5) == 0 || strncmp(argv[i], "--deeplink", 10) == 0
|| strncmp(argv[i], "--classic-gui", 13) == 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.
-#ifdef NO_GUI
- std::cout << "This version of JackTrip has been built without GUI support."
- << std::endl;
- std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
- << std::endl;
- std::exit(1);
-#else
- std::cout << "This version of JackTrip has been built with GUI support."
- << std::endl;
- std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
- << std::endl;
- std::exit(0);
-#endif
+ testGui = true;
}
}
- // If we have command line arguments and aren't forcing the GUI run on the command
- // line.
- if (argc == 1 || forceGui) {
#ifdef NO_GUI
- if (forceGui) {
- std::cout << "This version of jacktrip has not been built with GUI support."
- << std::endl;
- std::exit(1);
- } else {
- return new QCoreApplication(argc, argv);
- }
-#else
-#if defined(__unix__)
- // Check if X or Wayland environment variables are set.
- if (std::getenv("WAYLAND_DISPLAY") == nullptr
- && std::getenv("DISPLAY") == nullptr) {
- std::cout << "ERROR: Display not found. Make sure X or Wayland is running or "
- "try running jacktrip in command line mode."
- << std::endl;
- std::cout << "(To display a list of command line options run \"jacktrip -h\")"
- << std::endl;
- std::exit(1);
- }
-#endif
-#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- // Turn on high DPI support.
-#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
- JTApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-#endif
- // Fix for display scaling like 125% or 150% on Windows
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
- 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.
-#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
- QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-#endif
- // Fix for display scaling like 125% or 150% on Windows
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
-#if defined(NO_VS) && defined(_WIN32)
- QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
- Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor);
+ if (testGui) {
+ std::cout << "This version of JackTrip has been built without GUI support."
+ << std::endl;
+ std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
+ << std::endl;
+ std::exit(1);
+ }
+ if (forceGui) {
+ std::cout << "This version of jacktrip has not been built with GUI support."
+ << std::endl;
+ std::exit(1);
+ }
#else
- QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
- 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
- } else {
- return new QCoreApplication(argc, argv);
+ if (testGui) {
+ std::cout << "This version of JackTrip has been built with GUI support."
+ << std::endl;
+ std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
+ << std::endl;
+ std::exit(0);
}
-}
+ if (forceGui || argc == 1) {
+ return UserInterface::createApplication(argc, argv);
+ }
+#endif
-void qtMessageHandler([[maybe_unused]] QtMsgType type,
- [[maybe_unused]] const QMessageLogContext& context,
- const QString& msg)
-{
- std::cerr << msg.toStdString() << std::endl;
-#ifndef NO_GUI
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- // Writes to file in order to debug bundles and executables
- *ts << msg << Qt::endl;
-#endif // NO_VS && QT_VERSION
-#endif // NO_GUI
+ return new QCoreApplication(argc, argv);
}
void outputError(const QString& msg)
return false;
}
}
-
-bool isRunFromCmd()
-{
- // Get our parent process pid
- HANDLE h = NULL;
- PROCESSENTRY32 pe;
- ZeroMemory(&pe, sizeof(PROCESSENTRY32));
- DWORD pid = GetCurrentProcessId();
- DWORD ppid = 0;
- pe.dwSize = sizeof(PROCESSENTRY32);
- h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- if (Process32First(h, &pe)) {
- do {
- // Loop through the list of processes until we find ours.
- if (pe.th32ProcessID == pid) {
- ppid = pe.th32ParentProcessID;
- break;
- }
- } while (Process32Next(h, &pe));
- }
- CloseHandle(h);
-
- // Get the name of our parent process;
- char pname[MAX_PATH] = {0};
- DWORD size = MAX_PATH;
- h = NULL;
- h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ppid);
- if (h) {
- if (QueryFullProcessImageNameA(h, 0, pname, &size)) {
- CloseHandle(h);
-
- // Check if our parent process is a command line.
- if (size >= 14 && strncmp(pname + size - 14, "powershell.exe", 14) == 0) {
- return true;
- }
- if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) {
- return true;
- }
- if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) {
- return true;
- }
- // a few extras for msys/cygwin/etc
- if (size >= 8 && strncmp(pname + size - 8, "bash.exe", 8) == 0) {
- return true;
- }
- if (size >= 6 && strncmp(pname + size - 6, "sh.exe", 6) == 0) {
- return true;
- }
- if (size >= 7 && strncmp(pname + size - 7, "zsh.exe", 7) == 0) {
- return true;
- }
- } else {
- CloseHandle(h);
- }
- }
-
- return false;
-}
#endif
int main(int argc, char* argv[])
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
QScopedPointer<JackTrip> jackTrip;
QScopedPointer<UdpHubListener> udpHub;
-#ifndef NO_GUI
- QSharedPointer<QJackTrip> window;
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- QQuickStyle::setStyle("Basic");
-#endif // QT_VERSION
-
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- QSharedPointer<VirtualStudio> vsPtr;
- QScopedPointer<VsDeeplink> vsDeeplinkPtr;
-#endif // NO_VS && QT_VERSION
+ QSharedPointer<Settings> cliSettings;
-#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- if (qobject_cast<JTApplication*>(app.data())) {
-#else
- if (qobject_cast<QApplication*>(app.data())) {
-#endif
- // Start the GUI if there are no command line options.
-#ifdef _WIN32
- // Remove the console that appears if we're on windows and not running from a
- // command line.
- if (!isRunFromCmd()) {
- std::cout << "This extra window is caused by a bug in Microsoft Windows. "
- << "It can safely be ignored or closed." << std::endl
- << std::endl
- << "To fix this bug, please upgrade to the latest version of "
- << "Windows Terminal available in the Microsoft App Store:"
- << std::endl
- << "https://aka.ms/terminal" << std::endl;
-
- FreeConsole();
- }
-#endif // _WIN32
- app->setOrganizationName(QStringLiteral("jacktrip"));
- app->setOrganizationDomain(QStringLiteral("jacktrip.org"));
- app->setApplicationName(QStringLiteral("JackTrip"));
- app->setApplicationVersion(gVersion);
-
- QSharedPointer<Settings> cliSettings;
+#ifndef NO_GUI
+ QScopedPointer<UserInterface> interface;
+ QApplication* guiApp = dynamic_cast<QApplication*>(app.data());
+ if (guiApp != nullptr) {
+ // Start the GUI
cliSettings.reset(new Settings(true));
cliSettings->parseInput(argc, argv);
+ interface.reset(new UserInterface(cliSettings));
+ interface->start(guiApp);
+ return app->exec();
+ }
+#endif // NO_GUI
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- // Register clipboard Qml type
- qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
-
- // prepare handler for deeplinks jacktrip://join/<StudioID>
- vsDeeplinkPtr.reset(new VsDeeplink(cliSettings->getDeeplink()));
- if (!vsDeeplinkPtr->getDeeplink().isEmpty()) {
- bool readyForExit = vsDeeplinkPtr->waitForReady();
- if (readyForExit)
- return 0;
- }
-
- // Check which mode we are running in
- QSettings settings;
- 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));
-#endif // NO_VS
- QObject::connect(window.data(), &QJackTrip::signalExit, app.data(),
- &QCoreApplication::quit, Qt::QueuedConnection);
-
-#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
- vsPtr.reset(new VirtualStudio(uiMode == QJackTrip::UNSET));
- 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);
- vsDeeplinkPtr->readyForSignals();
-
- if (uiMode == QJackTrip::UNSET) {
- vsPtr->show();
- } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) {
- vsPtr->show();
- } else {
- window->show();
- }
-
- // Log to file
- QString logPath(
- QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
- QDir logDir;
- if (!logDir.exists(logPath)) {
- logDir.mkpath(logPath);
- }
- QString fileLoc(logPath.append("/log.txt"));
- qDebug() << "Log file location:" << fileLoc;
- outFile.setFileName(fileLoc);
- if (!outFile.open(QIODevice::WriteOnly | QIODevice::Append)) {
- qDebug() << "Log file open failed:" << outFile.errorString();
- }
- ts = new QTextStream(&outFile);
- qInstallMessageHandler(qtMessageHandler);
-#else
- window->show();
-#endif // NO_VS
+ // Otherwise use the non-GUI version, and parse our command line.
+ try {
+ cliSettings.reset(new Settings(false));
+ cliSettings->parseInput(argc, argv);
-#if !defined(NO_UPDATER) && !defined(__unix__)
#ifndef PSI
-#if defined(NO_VS) || QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
-
- // This wasn't set up earlier in NO_VS builds. Create it here.
- QSettings settings;
-#endif
- QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable")
- .toString()
- .toLower();
- QString baseUrl = QStringLiteral("https://files.jacktrip.org/app-releases/%1")
- .arg(updateChannel);
-#else
- QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip");
-#endif // PSI
- // Setup auto-update feed
- dblsqd::Feed* feed = new dblsqd::Feed();
-#ifdef Q_OS_WIN
- feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "win")));
-#endif
-#ifdef Q_OS_MACOS
- feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "mac")));
-#endif
- if (feed) {
- dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
- updateDialog->setIcon(":/qjacktrip/icon.png");
+ if (gVerboseFlag) {
+ QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true"));
}
-#endif // NO_UPDATER
- } else {
-#endif // NO_GUI
- // Otherwise use the non-GUI version, and parse our command line.
- try {
- Settings settings;
- settings.parseInput(argc, argv);
-
-#ifndef PSI
- if (gVerboseFlag) {
- QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true"));
- }
#endif
- // Either start our hub server or our jacktrip process as appropriate.
- if (settings.isHubServer()) {
- udpHub.reset(settings.getConfiguredHubServer());
- if (gVerboseFlag)
- std::cout << "Settings:startJackTrip before udphub->start"
- << std::endl;
- QObject::connect(udpHub.data(), &UdpHubListener::signalStopped,
- app.data(), &QCoreApplication::quit,
- Qt::QueuedConnection);
- QObject::connect(udpHub.data(), &UdpHubListener::signalError,
- outputError);
- QObject::connect(udpHub.data(), &UdpHubListener::signalError, app.data(),
- &QCoreApplication::quit, Qt::QueuedConnection);
+ // Either start our hub server or our jacktrip process as appropriate.
+ if (cliSettings->isHubServer()) {
+ udpHub.reset(cliSettings->getConfiguredHubServer());
+ if (gVerboseFlag)
+ std::cout << "Settings:startJackTrip before udphub->start" << std::endl;
+ QObject::connect(udpHub.data(), &UdpHubListener::signalStopped, app.data(),
+ &QCoreApplication::quit, Qt::QueuedConnection);
+ QObject::connect(udpHub.data(), &UdpHubListener::signalError, outputError);
+ QObject::connect(udpHub.data(), &UdpHubListener::signalError, app.data(),
+ &QCoreApplication::quit, Qt::QueuedConnection);
#ifndef _WIN32
- setupUnixSignalHandler(UdpHubListener::sigIntHandler);
+ setupUnixSignalHandler(UdpHubListener::sigIntHandler);
#else
isHubServer = true;
SetConsoleCtrlHandler(windowsCtrlHandler, true);
-#endif
- udpHub->start();
- } else {
- jackTrip.reset(settings.getConfiguredJackTrip());
- if (gVerboseFlag)
- std::cout << "Settings:startJackTrip before mJackTrip->startProcess"
- << std::endl;
- QObject::connect(jackTrip.data(), &JackTrip::signalProcessesStopped,
- app.data(), &QCoreApplication::quit,
- Qt::QueuedConnection);
- QObject::connect(jackTrip.data(), &JackTrip::signalError, outputError);
- QObject::connect(jackTrip.data(), &JackTrip::signalError, app.data(),
- &QCoreApplication::quit, Qt::QueuedConnection);
+#endif // _WIN32
+ udpHub->start();
+ } else {
+ jackTrip.reset(cliSettings->getConfiguredJackTrip());
+ if (gVerboseFlag)
+ std::cout << "Settings:startJackTrip before mJackTrip->startProcess"
+ << std::endl;
+ QObject::connect(jackTrip.data(), &JackTrip::signalProcessesStopped,
+ app.data(), &QCoreApplication::quit, Qt::QueuedConnection);
+ QObject::connect(jackTrip.data(), &JackTrip::signalError, outputError);
+ QObject::connect(jackTrip.data(), &JackTrip::signalError, app.data(),
+ &QCoreApplication::quit, Qt::QueuedConnection);
+
#ifndef _WIN32
- setupUnixSignalHandler(JackTrip::sigIntHandler);
+ setupUnixSignalHandler(JackTrip::sigIntHandler);
#else
std::cout << SetConsoleCtrlHandler(windowsCtrlHandler, true) << std::endl;
-#endif
-#ifdef WAIRTOHUB // WAIR
- jackTrip->startProcess(
- 0); // for WAIR compatibility, ID in jack client name
+#endif // _WIN32
+
+#ifdef WAIRTOHUB // WAIR
+ jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name
#else
jackTrip->startProcess();
#endif // endwhere
- }
-
- if (gVerboseFlag)
- std::cout << "step 6" << std::endl;
- if (gVerboseFlag)
- std::cout << "jmain before app->exec()" << std::endl;
- } catch (const std::exception& e) {
- std::cerr << "ERROR:" << std::endl;
- std::cerr << e.what() << std::endl;
- std::cerr << "Exiting JackTrip..." << std::endl;
- std::cerr << gPrintSeparator << std::endl;
- return -1;
}
-#ifndef NO_GUI
+
+ if (gVerboseFlag)
+ std::cout << "step 6" << std::endl;
+ if (gVerboseFlag)
+ std::cout << "jmain before app->exec()" << std::endl;
+ } catch (const std::exception& e) {
+ std::cerr << "ERROR:" << std::endl;
+ std::cerr << e.what() << std::endl;
+ std::cerr << "Exiting JackTrip..." << std::endl;
+ std::cerr << gPrintSeparator << std::endl;
+ return -1;
}
-#endif // NO_GUI
return app->exec();
}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Window {
+ id: aboutWindow
+ title: "About JackTrip"
+ visible: false
+
+ width: 600 * virtualstudio.uiScale
+ height: 570 * virtualstudio.uiScale
+
+ property int fontTitle: 20
+ property int fontMedium: 12
+ property int fontSmall: 10
+ property int fontTiny: 8
+
+ property string buttonColour: "#F2F3F3"
+ property string buttonHoverColour: "#E7E8E8"
+ property string buttonPressedColour: "#E7E8E8"
+ property string buttonStroke: "#EAEBEB"
+ property string buttonHoverStroke: "#B0B5B5"
+ property string buttonPressedStroke: "#B0B5B5"
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
+ property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+
+ Rectangle {
+ width: parent.width
+ height: parent.height
+ color: backgroundColour
+
+ Rectangle {
+ id: aboutJackTripLogo
+ x: 0; y: 0;
+ width: 122 * virtualstudio.uiScale
+ height: 108 * virtualstudio.uiScale
+ color: backgroundColour
+ Image {
+ id: aboutLogoImage
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ source: "logo.svg"
+ width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale
+ sourceSize: Qt.size(aboutLogoImage.width,aboutLogoImage.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ Item {
+ id: aboutContent
+ x: 122 * virtualstudio.uiScale;
+ y: 0;
+ width: parent.width - aboutJackTripLogo.width - (32 * virtualstudio.uiScale)
+ height: parent.height
+
+ Text {
+ id: aboutHeader
+ anchors.top: parent.top
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: parent.left
+ width: parent.width
+ text: "JackTrip Desktop App"
+ font {family: "Poppins"; pixelSize: fontTitle * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: aboutVersion
+ anchors.top: aboutHeader.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: parent.left
+ width: parent.width
+ text: "Version " + virtualstudio.versionString
+ font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: aboutBuildInfo
+ anchors.top: aboutVersion.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: parent.left
+ width: parent.width
+ text: virtualstudio.buildString
+ font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: aboutCopyright
+ anchors.top: aboutBuildInfo.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: parent.left
+ width: parent.width
+ text: virtualstudio.copyrightString
+ font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Button {
+ id: aboutCloseButton
+ anchors.top: aboutCopyright.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.left: parent.left
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ onClicked: () => {
+ aboutWindow.visible = false;
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: aboutCloseButton.down ? buttonPressedColour : (aboutCloseButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: aboutCloseButton.down ? buttonPressedStroke : (aboutCloseButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: "Close"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: virtualstudio
+
+ function onOpenAboutWindow() {
+ aboutWindow.visible = true;
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ id: appIcon
+
+ property alias icon: btn.icon
+ property string color: ""
+ property string defaultColor: virtualstudio.darkMode ? "#CCCCCC" : "#333333"
+ signal clicked()
+
+ Button {
+ id: btn
+ anchors.fill: parent
+ anchors.centerIn: parent
+ topInset: 0
+ leftInset: 0
+ rightInset: 0
+ bottomInset: 0
+ padding: 0
+
+ background: Rectangle { color: "transparent" }
+ icon.color: color ? color : defaultColor
+ icon.width: parent.width
+ icon.height: parent.height
+ display: AbstractButton.IconOnly
+ onClicked: appIcon.clicked()
+ }
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 AudioInterfaceMode.h
+ * \author Matt Horton
+ * \date December 2022
+ */
+
+#ifndef AUDIOINTERFACEMODE_H
+#define AUDIOINTERFACEMODE_H
+
+enum class AudioInterfaceMode {
+ JACK, ///< Jack Mode
+ RTAUDIO, ///< RtAudio Mode
+ ALL,
+ NONE
+};
+
+#ifdef RT_AUDIO
+#ifndef NO_JACK
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::ALL;
+#else
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::RTAUDIO;
+#endif
+#else
+#ifndef NO_JACK
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::JACK;
+#else
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::NONE;
+#endif
+#endif
+
+template<AudioInterfaceMode backend>
+constexpr auto isBackendAvailable()
+{
+ if constexpr (backend == AudioInterfaceMode::RTAUDIO) {
+ if (mode == AudioInterfaceMode::RTAUDIO || mode == AudioInterfaceMode::ALL) {
+ return true;
+ } else {
+ return false;
+ }
+ } else if constexpr (backend == AudioInterfaceMode::JACK) {
+ if (mode == AudioInterfaceMode::JACK || mode == AudioInterfaceMode::ALL) {
+ return true;
+ } else {
+ return false;
+ }
+ } else if constexpr (backend == AudioInterfaceMode::ALL) {
+ if (mode == AudioInterfaceMode::ALL) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+#endif // AUDIOINTERFACEMODE_H
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ width: parent.width
+ height: parent.height
+ color: backgroundColour
+
+ property bool connected: false
+ property bool showMeters: true
+ property bool showTestAudio: true
+
+ property int fontBig: 20
+ property int fontMedium: 13
+ property int fontSmall: 11
+ property int fontExtraSmall: 8
+
+ property int leftMargin: 48
+ property int rightMargin: 16
+ property int bottomToolTipMargin: 8
+ property int rightToolTipMargin: 4
+ property int buttonWidth: 103
+ property int buttonHeight: 25
+
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+ property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+ property string toolTipTextColour: textColour
+
+ property string errorFlagColour: "#DB0A0A"
+ property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+ function getCurrentInputDeviceIndex () {
+ if (audio.inputDevice === "") {
+ return audio.inputComboModel.findIndex(elem => elem.type === "element");
+ }
+
+ let idx = audio.inputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.inputDevice);
+ if (idx < 0) {
+ idx = audio.inputComboModel.findIndex(elem => elem.type === "element");
+ }
+ return idx;
+ }
+
+ function getCurrentOutputDeviceIndex() {
+ if (audio.outputDevice === "") {
+ return audio.outputComboModel.findIndex(elem => elem.type === "element");
+ }
+
+ let idx = audio.outputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.outputDevice);
+ if (idx < 0) {
+ idx = audio.outputComboModel.findIndex(elem => elem.type === "element");
+ }
+ return idx;
+ }
+
+ function getCurrentInputChannelsIndex() {
+ let idx = audio.inputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseInputChannel
+ && elem.numChannels === audio.numInputChannels);
+ if (idx < 0) {
+ idx = 0;
+ }
+ return idx;
+ }
+
+ function getCurrentOutputChannelsIndex() {
+ let idx = audio.outputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseOutputChannel
+ && elem.numChannels === audio.numOutputChannels);
+ if (idx < 0) {
+ idx = 0;
+ }
+ return idx;
+ }
+
+ function getCurrentMixModeIndex() {
+ let idx = audio.inputMixModeComboModel.findIndex(elem => elem.value === audio.inputMixMode);
+ if (idx < 0) {
+ idx = 0;
+ }
+ return idx;
+ }
+
+ Loader {
+ anchors.fill: parent
+ sourceComponent: audio.audioBackend == "JACK" ? usingJACK : ((!audio.deviceModelsInitialized || audio.scanningDevices) ? scanningDevices : usingRtAudio);
+ }
+
+ Component {
+ id: usingRtAudio
+
+ Item {
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: 24 * virtualstudio.uiScale
+ anchors.right: parent.right
+
+ Rectangle {
+ id: leftSpacer
+ x: 0; y: 0
+ width: 144 * virtualstudio.uiScale
+ height: 0
+ color: "transparent"
+ }
+
+ Text {
+ id: outputLabel
+ x: 0; y: 0
+ text: "Output Device"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ bottomPadding: 10 * virtualstudio.uiScale
+ color: textColour
+ }
+
+ InfoTooltip {
+ id: outputHelpIcon
+ anchors.left: outputLabel.right
+ anchors.bottom: outputLabel.top
+ anchors.bottomMargin: -8 * virtualstudio.uiScale
+ size: 16 * virtualstudio.uiScale
+ content: qsTr("How you'll hear the studio audio")
+ }
+
+ AppIcon {
+ id: headphonesIcon
+ anchors.left: outputLabel.left
+ anchors.top: outputLabel.bottom
+ width: 28 * virtualstudio.uiScale
+ height: 28 * virtualstudio.uiScale
+ icon.source: "headphones.svg"
+ }
+
+ ComboBox {
+ id: outputCombo
+ anchors.left: leftSpacer.right
+ anchors.verticalCenter: outputLabel.verticalCenter
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale
+ model: audio.outputComboModel
+ currentIndex: getCurrentOutputDeviceIndex()
+ delegate: ItemDelegate {
+ required property var modelData
+ required property int index
+
+ leftPadding: 0
+
+ width: parent.width
+ contentItem: Text {
+ leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
+ text: modelData.text
+ font.bold: modelData.type === "header"
+ }
+ highlighted: outputCombo.highlightedIndex === index
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (modelData.type == "element") {
+ outputCombo.currentIndex = index
+ outputCombo.popup.close()
+ audio.outputDevice = modelData.text
+ if (modelData.category.startsWith("Low-Latency")) {
+ let inputComboIdx = inputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
+ if (inputComboIdx !== null && inputComboIdx !== undefined) {
+ inputCombo.currentIndex = inputComboIdx;
+ audio.inputDevice = modelData.text
+ }
+ }
+ if (connected) {
+ virtualstudio.triggerReconnect(false);
+ } else {
+ audio.validateDevices()
+ audio.restartAudio()
+ }
+ }
+ }
+ }
+ }
+ contentItem: Text {
+ leftPadding: 12
+ font: outputCombo.font
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: outputCombo.model[outputCombo.currentIndex]!=undefined && outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : ""
+ }
+ }
+
+ Meter {
+ id: outputDeviceMeters
+ anchors.left: outputCombo.left
+ anchors.right: outputCombo.right
+ anchors.top: outputCombo.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ model: showMeters ? audio.outputMeterLevels : [0, 0]
+ clipped: audio.outputClipped
+ visible: showMeters
+ enabled: audio.audioReady && !Boolean(audio.devicesError)
+ }
+
+ VolumeSlider {
+ id: outputSlider
+ anchors.left: outputCombo.left
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.top: outputDeviceMeters.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ labelText: "Studio"
+ tooltipText: "How loudly you hear other participants"
+ showLabel: false
+ sliderEnabled: true
+ visible: showMeters
+ }
+
+ Text {
+ id: outputChannelsLabel
+ anchors.left: outputCombo.left
+ anchors.right: outputCombo.horizontalCenter
+ anchors.top: showMeters ? outputSlider.bottom : outputCombo.bottom
+ anchors.topMargin: 12 * virtualstudio.uiScale
+ textFormat: Text.RichText
+ text: "Output Channel(s)"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: outputChannelsCombo
+ anchors.left: outputCombo.left
+ anchors.right: outputCombo.horizontalCenter
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ anchors.top: outputChannelsLabel.bottom
+ anchors.topMargin: 4 * virtualstudio.uiScale
+ model: audio.outputChannelsComboModel
+ enabled: audio.outputChannelsComboModel.length > 1
+ currentIndex: getCurrentOutputChannelsIndex()
+ delegate: ItemDelegate {
+ required property var modelData
+ required property int index
+ width: parent.width
+ contentItem: Text {
+ text: modelData.label
+ }
+ highlighted: outputChannelsCombo.highlightedIndex === index
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ outputChannelsCombo.currentIndex = index
+ outputChannelsCombo.popup.close()
+ audio.baseOutputChannel = modelData.baseChannel
+ audio.numOutputChannels = modelData.numChannels
+ if (connected) {
+ virtualstudio.triggerReconnect(false);
+ } else {
+ audio.validateDevices()
+ audio.restartAudio()
+ }
+ }
+ }
+ }
+ contentItem: Text {
+ leftPadding: 12
+ font: inputCombo.font
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || ""
+ }
+ }
+
+ Button {
+ id: testOutputAudioButton
+ visible: showTestAudio
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { audio.playOutputAudio() }
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: outputChannelsCombo.verticalCenter
+ width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Play Test Tone"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Rectangle {
+ id: divider1
+ anchors.top: showTestAudio ? testOutputAudioButton.bottom : outputChannelsCombo.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale
+ color: "#E0E0E0"
+ }
+
+ Text {
+ id: inputLabel
+ anchors.left: outputLabel.left
+ anchors.top: divider1.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ text: "Input Device"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ bottomPadding: 10 * virtualstudio.uiScale
+ color: textColour
+ }
+
+ InfoTooltip {
+ id: inputHelpIcon
+ anchors.left: inputLabel.right
+ anchors.bottom: inputLabel.top
+ anchors.bottomMargin: -8 * virtualstudio.uiScale
+ size: 16 * virtualstudio.uiScale
+ content: qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)")
+ }
+
+ AppIcon {
+ id: microphoneIcon
+ anchors.left: inputLabel.left
+ anchors.top: inputLabel.bottom
+ width: 32 * virtualstudio.uiScale
+ height: 32 * virtualstudio.uiScale
+ icon.source: "mic.svg"
+ }
+
+ ComboBox {
+ id: inputCombo
+ model: audio.inputComboModel
+ currentIndex: getCurrentInputDeviceIndex()
+ anchors.left: outputCombo.left
+ anchors.right: outputCombo.right
+ anchors.verticalCenter: inputLabel.verticalCenter
+ delegate: ItemDelegate {
+ required property var modelData
+ required property int index
+
+ leftPadding: 0
+
+ width: parent.width
+ contentItem: Text {
+ leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
+ text: modelData.text
+ font.bold: modelData.type === "header"
+ }
+ highlighted: inputCombo.highlightedIndex === index
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (modelData.type == "element") {
+ inputCombo.currentIndex = index
+ inputCombo.popup.close()
+ audio.inputDevice = modelData.text
+ if (modelData.category.startsWith("Low-Latency")) {
+ let outputComboIdx = outputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
+ if (outputComboIdx !== null && outputComboIdx !== undefined) {
+ outputCombo.currentIndex = outputComboIdx;
+ audio.outputDevice = modelData.text
+ }
+ }
+ if (connected) {
+ virtualstudio.triggerReconnect(false);
+ } else {
+ audio.validateDevices()
+ audio.restartAudio()
+ }
+ }
+ }
+ }
+ }
+ contentItem: Text {
+ leftPadding: 12
+ font: inputCombo.font
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: inputCombo.model[inputCombo.currentIndex] != undefined && inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : ""
+ }
+ }
+
+ Meter {
+ id: inputDeviceMeters
+ anchors.left: inputCombo.left
+ anchors.right: inputCombo.right
+ anchors.top: inputCombo.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ model: showMeters ? audio.inputMeterLevels : [0, 0]
+ clipped: audio.inputClipped
+ visible: showMeters
+ enabled: audio.audioReady && !Boolean(audio.devicesError)
+ }
+
+ VolumeSlider {
+ id: inputSlider
+ anchors.left: inputCombo.left
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.top: inputDeviceMeters.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ labelText: "Send"
+ tooltipText: "How loudly other participants hear you"
+ showLabel: false
+ sliderEnabled: true
+ visible: showMeters
+ }
+
+ Button {
+ id: hiddenInputButton
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: inputSlider.verticalCenter
+ width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ visible: false
+ }
+
+ Text {
+ id: inputChannelsLabel
+ anchors.left: inputCombo.left
+ anchors.right: inputCombo.horizontalCenter
+ anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
+ anchors.topMargin: 12 * virtualstudio.uiScale
+ textFormat: Text.RichText
+ text: "Input Channel(s)"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: inputChannelsCombo
+ anchors.left: inputCombo.left
+ anchors.right: inputCombo.horizontalCenter
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ anchors.top: inputChannelsLabel.bottom
+ anchors.topMargin: 4 * virtualstudio.uiScale
+ model: audio.inputChannelsComboModel
+ enabled: audio.inputChannelsComboModel.length > 1
+ currentIndex: getCurrentInputChannelsIndex()
+ delegate: ItemDelegate {
+ required property var modelData
+ required property int index
+ width: parent.width
+ contentItem: Text {
+ text: modelData.label
+ }
+ highlighted: inputChannelsCombo.highlightedIndex === index
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ inputChannelsCombo.currentIndex = index
+ inputChannelsCombo.popup.close()
+ audio.baseInputChannel = modelData.baseChannel
+ audio.numInputChannels = modelData.numChannels
+ if (connected) {
+ virtualstudio.triggerReconnect(false);
+ } else {
+ audio.validateDevices()
+ audio.restartAudio()
+ }
+ }
+ }
+ }
+ contentItem: Text {
+ leftPadding: 12
+ font: inputCombo.font
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || ""
+ }
+ }
+
+ Text {
+ id: inputMixModeLabel
+ anchors.left: inputCombo.horizontalCenter
+ anchors.right: inputCombo.right
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
+ anchors.topMargin: 12 * virtualstudio.uiScale
+ textFormat: Text.RichText
+ text: "Mono / Stereo"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: inputMixModeCombo
+ anchors.left: inputCombo.horizontalCenter
+ anchors.right: inputCombo.right
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ anchors.top: inputMixModeLabel.bottom
+ anchors.topMargin: 4 * virtualstudio.uiScale
+ model: audio.inputMixModeComboModel
+ enabled: audio.inputMixModeComboModel.length > 1
+ currentIndex: getCurrentMixModeIndex()
+ delegate: ItemDelegate {
+ required property var modelData
+ required property int index
+ width: parent.width
+ contentItem: Text {
+ text: modelData.label
+ }
+ highlighted: inputMixModeCombo.highlightedIndex === index
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ inputMixModeCombo.currentIndex = index
+ inputMixModeCombo.popup.close()
+ audio.inputMixMode = audio.inputMixModeComboModel[index].value
+ if (connected) {
+ virtualstudio.triggerReconnect(false);
+ } else {
+ audio.validateDevices()
+ audio.restartAudio()
+ }
+ }
+ }
+ }
+ contentItem: Text {
+ leftPadding: 12
+ font: inputCombo.font
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || ""
+ }
+ }
+
+ Text {
+ id: inputChannelHelpMessage
+ anchors.left: inputChannelsCombo.left
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.right: inputChannelsCombo.right
+ anchors.top: inputChannelsCombo.bottom
+ anchors.topMargin: 8 * virtualstudio.uiScale
+ textFormat: Text.RichText
+ wrapMode: Text.WordWrap
+ text: audio.inputChannelsComboModel.length > 1 ? "Choose up to 2 channels" : "Only 1 channel available"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Text {
+ id: inputMixModeHelpMessage
+ anchors.left: inputMixModeCombo.left
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.right: inputMixModeCombo.right
+ anchors.top: inputMixModeCombo.bottom
+ anchors.topMargin: 8 * virtualstudio.uiScale
+ textFormat: Text.RichText
+ wrapMode: Text.WordWrap
+ text: (() => {
+ if (audio.inputMixMode === 2) {
+ return "Treat the channels as Left and Right signals, coming through each speaker separately.";
+ } else if (audio.inputMixMode === 3) {
+ return "Combine the channels into one central channel coming through both speakers.";
+ } else if (audio.inputMixMode === 1) {
+ return "Send a single channel of audio";
+ } else {
+ return "";
+ }
+ })()
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Connections {
+ target: audio
+ // anything that sets currentIndex to the value of a function needs
+ // to be manually updated whenever there is a change to any vars it uses
+ function onInputDeviceChanged() {
+ inputCombo.currentIndex = getCurrentInputDeviceIndex();
+ }
+ function onOutputDeviceChanged() {
+ outputCombo.currentIndex = getCurrentOutputDeviceIndex();
+ }
+ function onNumInputChannelsChanged() {
+ inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
+ }
+ function onBaseInputChannelChanged() {
+ inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
+ }
+ function onNumOutputChannelsChanged() {
+ outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
+ }
+ function onBaseOutputChannelChanged() {
+ outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
+ }
+ function onInputMixModeChanged() {
+ inputMixModeCombo.currentIndex = getCurrentMixModeIndex();
+ }
+ }
+ }
+ }
+
+ Component {
+ id: usingJACK
+
+ Item {
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: leftMargin * virtualstudio.uiScale
+ anchors.right: parent.right
+
+ Text {
+ id: jackLabel
+ x: 0; y: 0
+ width: parent.width - rightMargin * virtualstudio.uiScale
+ text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ wrapMode: Text.WordWrap
+ color: textColour
+ }
+
+ Text {
+ id: jackOutputLabel
+ anchors.left: jackLabel.left
+ anchors.top: jackLabel.bottom
+ anchors.topMargin: 48 * virtualstudio.uiScale
+ width: 144 * virtualstudio.uiScale
+ text: "Output Volume"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ wrapMode: Text.WordWrap
+ bottomPadding: 10 * virtualstudio.uiScale
+ color: textColour
+ }
+
+ AppIcon {
+ id: jackHeadphonesIcon
+ anchors.left: jackOutputLabel.left
+ anchors.top: jackOutputLabel.bottom
+ width: 28 * virtualstudio.uiScale
+ height: 28 * virtualstudio.uiScale
+ icon.source: "headphones.svg"
+ }
+
+ Meter {
+ id: jackOutputMeters
+ anchors.left: jackOutputLabel.right
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: jackOutputLabel.verticalCenter
+ height: 24 * virtualstudio.uiScale
+ model: showMeters ? audio.outputMeterLevels : [0, 0]
+ clipped: audio.outputClipped
+ enabled: audio.audioReady && !Boolean(audio.devicesError)
+ }
+
+ Button {
+ id: jackTestOutputAudioButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { audio.playOutputAudio() }
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter
+ width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Play Test Tone"
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ VolumeSlider {
+ id: jackOutputVolumeSlider
+ anchors.left: jackOutputMeters.left
+ anchors.right: jackTestOutputAudioButton.left
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.top: jackOutputMeters.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ labelText: "Studio"
+ tooltipText: "How loudly you hear other participants"
+ showLabel: false
+ sliderEnabled: true
+ }
+
+ Text {
+ id: jackInputLabel
+ anchors.left: jackLabel.left
+ anchors.top: jackOutputVolumeSlider.bottom
+ anchors.topMargin: 48 * virtualstudio.uiScale
+ width: 144 * virtualstudio.uiScale
+ text: "Input Volume"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ wrapMode: Text.WordWrap
+ bottomPadding: 10 * virtualstudio.uiScale
+ color: textColour
+ }
+
+ AppIcon {
+ id: jackMicrophoneIcon
+ anchors.left: jackInputLabel.left
+ anchors.top: jackInputLabel.bottom
+ width: 32 * virtualstudio.uiScale
+ height: 32 * virtualstudio.uiScale
+ icon.source: "mic.svg"
+ }
+
+ Meter {
+ id: jackInputMeters
+ anchors.left: jackInputLabel.right
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: jackInputLabel.verticalCenter
+ height: 24 * virtualstudio.uiScale
+ model: showMeters ? audio.inputMeterLevels : [0, 0]
+ clipped: audio.inputClipped
+ enabled: audio.audioReady && !Boolean(audio.devicesError)
+ }
+
+ VolumeSlider {
+ id: jackInputVolumeSlider
+ anchors.left: jackInputMeters.left
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.top: jackInputMeters.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ labelText: "Send"
+ tooltipText: "How loudly other participants hear you"
+ showLabel: false
+ sliderEnabled: true
+ }
+
+ Button {
+ id: jackHiddenInputButton
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: jackInputVolumeSlider.verticalCenter
+ width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ visible: false
+ }
+ }
+ }
+
+ Component {
+ id: noBackend
+
+ Item {
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: leftMargin * virtualstudio.uiScale
+ anchors.right: parent.right
+
+ Text {
+ id: noBackendLabel
+ x: 0; y: 0
+ width: parent.width - (16 * virtualstudio.uiScale)
+ text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag."
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ wrapMode: Text.WordWrap
+ color: textColour
+ }
+ }
+ }
+
+ Component {
+ id: scanningDevices
+
+ Item {
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: leftMargin * virtualstudio.uiScale
+ anchors.right: parent.right
+
+ Text {
+ id: scanningDevicesLabel
+ x: 0; y: 0
+ width: parent.width - (16 * virtualstudio.uiScale)
+ text: "Scanning audio devices..."
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ wrapMode: Text.WordWrap
+ color: textColour
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ Rectangle {
+ width: parent.width; height: parent.height
+ color: backgroundColour
+ }
+
+ property int buttonHeight: 25
+ property int buttonWidth: 103
+ property int extraSettingsButtonWidth: 16
+ property int emptyListMessageWidth: 450
+ property int createMessageTopMargin: 16
+ property int createButtonTopMargin: 24
+ property int fontBig: 28
+ property int fontMedium: 11
+
+ property int scrollY: 0
+
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D"
+
+ function refresh() {
+ scrollY = studioListView.contentY;
+ var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY);
+ if (currentIndex == -1) {
+ currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
+ }
+ virtualstudio.refreshStudios(currentIndex, true)
+ }
+
+ Component {
+ id: footer
+ Rectangle {
+ height: 16 * virtualstudio.uiScale
+ x: 16 * virtualstudio.uiScale
+ width: parent.width - (2 * x)
+ color: backgroundColour
+ }
+ }
+
+ ListView {
+ id: studioListView
+ x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale
+ spacing: 16 * virtualstudio.uiScale
+ header: footer
+ footer: footer
+ model: virtualstudio.serverModel
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ delegate: Studio {
+ x: 16 * virtualstudio.uiScale
+ width: studioListView.width - (2 * x)
+ serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : ""
+ flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag
+ studioName: modelData.name
+ publicStudio: modelData.isPublic
+ admin: modelData.isAdmin
+ available: modelData.canConnect
+ connected: false
+ studioId: modelData.id ? modelData.id : ""
+ streamId: modelData.streamId ? modelData.streamId : ""
+ inviteKeyString: modelData.inviteKey ? modelData.inviteKey : ""
+ sampleRate: modelData.sampleRate
+ }
+
+ section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} }
+
+ // Show sectionHeading if there are no Studios in list
+ SectionHeading {
+ id: emptyListSectionHeading
+ listIsEmpty: true
+ visible: parent.count == 0
+ }
+
+ Text {
+ id: emptyListMessage
+ visible: parent.count == 0
+ text: virtualstudio.refreshInProgress ? "Loading Studios..." : "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
+ }
+
+ Button {
+ id: resetFiltersButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ visible: parent.count == 0
+ onClicked: {
+ virtualstudio.showSelfHosted = true;
+ virtualstudio.showInactive = true;
+ refresh();
+ }
+ anchors.top: emptyListMessage.bottom
+ anchors.topMargin: createButtonTopMargin
+ anchors.horizontalCenter: emptyListMessage.horizontalCenter
+ width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+ Text {
+ text: "Reset Filters"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ // Disable momentum scroll
+ MouseArea {
+ z: -1
+ anchors.fill: parent
+ onWheel: function (wheel) {
+ // trackpad
+ studioListView.contentY -= wheel.pixelDelta.y;
+ // mouse wheel
+ studioListView.contentY -= wheel.angleDelta.y;
+ studioListView.returnToBounds();
+ }
+ }
+
+ Component.onCompleted: {
+ // Customize scroll properties on different platforms
+ if (Qt.platform.os == "linux" || Qt.platform.os == "osx" ||
+ Qt.platform.os == "unix" || Qt.platform.os == "windows") {
+ var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}',
+ studioListView,
+ "dynamicSnippet1");
+ scrollBar.policy = ScrollBar.AlwaysOn;
+ ScrollBar.vertical = scrollBar;
+ }
+ }
+ }
+
+ Rectangle {
+ x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale
+ border.color: "#33979797"
+ color: backgroundColour
+
+ Button {
+ id: refreshButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { refresh() }
+ anchors.verticalCenter: parent.verticalCenter
+ x: 16 * virtualstudio.uiScale
+ width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+ Text {
+ text: "Refresh List"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Button {
+ id: aboutButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { virtualstudio.showAbout() }
+ anchors.verticalCenter: parent.verticalCenter
+ x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale)
+ width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+ Text {
+ text: "About"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Button {
+ id: settingsButton
+ text: "Settings"
+ palette.buttonText: textColour
+ icon {
+ source: "cog.svg";
+ color: textColour;
+ }
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); }
+ display: AbstractButton.TextBesideIcon
+ font {
+ family: "Poppins";
+ pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale;
+ }
+ leftPadding: 0
+ rightPadding: 4
+ spacing: 0
+ anchors.verticalCenter: parent.verticalCenter
+ x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale)
+ width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+ }
+ }
+
+ AboutWindow {
+ }
+
+ FeedbackSurvey {
+ }
+
+ Connections {
+ target: virtualstudio
+ // Need to do this to avoid layout issues with our section header.
+ function onNewScale() {
+ studioListView.positionViewAtEnd();
+ studioListView.positionViewAtBeginning();
+ scrollY = studioListView.contentY;
+ }
+ function onRefreshFinished(index) {
+ if (index == -1) {
+ studioListView.contentY = scrollY
+ } else {
+ studioListView.positionViewAtIndex(index, ListView.Beginning);
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ width: parent.width; height: parent.height
+ color: backgroundColour
+ clip: true
+
+ property int fontBig: 28
+ property int fontMedium: 12
+ property int fontSmall: 10
+ property int fontTiny: 8
+
+ property int rightMargin: 16
+ property int bottomToolTipMargin: 8
+ property int rightToolTipMargin: 4
+
+ property string saveButtonText: "#000000"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
+ property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+ property real muteButtonMutedLightnessValue: 0.24
+ property real muteButtonMutedSaturationValue: 0.73
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+ property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+ property string toolTipTextColour: textColour
+
+ property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+ property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+ property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: false
+ }
+
+ Rectangle {
+ id: audioSettingsView
+ width: parent.width;
+ height: parent.height;
+ color: backgroundColour
+ radius: 6 * virtualstudio.uiScale
+
+ DeviceRefreshButton {
+ id: refreshButton
+ anchors.top: parent.top;
+ anchors.topMargin: 16 * virtualstudio.uiScale;
+ anchors.right: parent.right;
+ anchors.rightMargin: 16 * virtualstudio.uiScale;
+ enabled: !audio.scanningDevices
+ onDeviceRefresh: function () {
+ virtualstudio.triggerReconnect(true);
+ }
+ }
+
+ Text {
+ text: "Restarting Audio"
+ anchors.verticalCenter: refreshButton.verticalCenter
+ anchors.right: refreshButton.left;
+ anchors.rightMargin: 16 * virtualstudio.uiScale;
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ visible: audio.scanningDevices
+ }
+
+ AudioSettings {
+ id: audioSettings
+ showMeters: false
+ showTestAudio: false
+ connected: true
+ height: 300 * virtualstudio.uiScale
+ anchors.top: refreshButton.bottom;
+ anchors.topMargin: 16 * virtualstudio.uiScale;
+ }
+ }
+
+ Button {
+ id: backButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
+ }
+ onClicked: {
+ virtualstudio.saveSettings();
+ virtualstudio.windowState = "connected";
+ }
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 16 * virtualstudio.uiScale;
+ anchors.left: parent.left
+ anchors.leftMargin: 16 * virtualstudio.uiScale;
+ width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale
+
+ Text {
+ text: "Back"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ DeviceWarning {
+ id: deviceWarning
+ anchors.left: backButton.right
+ anchors.leftMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 16 * virtualstudio.uiScale;
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.jacktrip.jacktrip 1.0
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property bool connecting: false
+
+ property int leftHeaderMargin: 16
+ property int fontBig: 28
+ property int fontMedium: 12
+ property int fontSmall: 10
+ property int fontTiny: 8
+
+ property int bodyMargin: 60
+ property int rightMargin: 16
+ property int bottomToolTipMargin: 8
+ property int rightToolTipMargin: 4
+
+ property string studioStatus: (virtualstudio.currentStudio.id === "" ? "" : virtualstudio.currentStudio.status)
+ property bool showReadyScreen: studioStatus === "Ready"
+ property bool showStartingScreen: studioStatus === "Starting"
+ property bool showStoppingScreen: (virtualstudio.currentStudio.id === "" ? false : (virtualstudio.currentStudio.isAdmin && !virtualstudio.currentStudio.enabled && virtualstudio.currentStudio.cloudId !== ""))
+ property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen
+
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+ property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+ property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string saveButtonBackgroundColour: "#F2F3F3"
+ property string saveButtonPressedColour: "#E7E8E8"
+ property string saveButtonStroke: "#EAEBEB"
+ property string saveButtonPressedStroke: "#B0B5B5"
+ property string saveButtonText: "#000000"
+
+ property string muteButtonMutedColor: "#FCB6B6"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
+ property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+ property real muteButtonMutedLightnessValue: 0.24
+ property real muteButtonMutedSaturationValue: 0.73
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+ property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+ property string toolTipTextColour: textColour
+ property string warningTextColour: "#DB0A0A"
+ property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+ property string meterGreen: "#61C554"
+ property string meterYellow: "#F5BF4F"
+ property string meterRed: "#F21B1B"
+
+ property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+
+ Loader {
+ id: studioWebLoader
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.bottom: deviceControlsGroup.top
+
+ property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+ property string studioId: virtualstudio.currentStudio.id
+
+ source: accessToken && studioId ? "Web.qml" : "WebNull.qml"
+ }
+
+ DeviceControlsGroup {
+ id: deviceControlsGroup
+ anchors.bottom: footer.top
+ }
+
+ Footer {
+ id: footer
+ anchors.bottom: parent.bottom
+ }
+}
--- /dev/null
+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 === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create?accessToken=${accessToken}&userId=${auth.userId}`
+
+ 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
+ visible: virtualstudio.serverModel.length > 0
+
+ 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
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+Item {
+ width: parent.width
+ height: parent.height
+ clip: true
+
+ required property bool isInput
+ property string muteButtonMutedColor: "#FCB6B6"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#D5D9D9"
+
+ Component {
+ id: controlIndicator
+
+ Button {
+ id: iconButton
+ width: 24 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+
+ anchors.left: parent.left
+ anchors.leftMargin: 8 * virtualstudio.uiScale
+
+ background: Rectangle {
+ color: isInput ? (audio.inputMuted ? muteButtonMutedColor : buttonColour) : "transparent"
+ width: 24 * virtualstudio.uiScale
+ radius: 4 * virtualstudio.uiScale
+ }
+
+ onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
+
+ AppIcon {
+ id: iconImage
+ anchors.centerIn: parent
+ width: 24 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ icon.source: isInput ? (audio.inputMuted ? "micoff.svg" : "mic.svg") : "headphones.svg"
+ color: isInput ? (audio.inputMuted ? "red" : ( virtualstudio.darkMode ? "#CCCCCC" : "#333333" )) : (virtualstudio.darkMode ? "#CCCCCC" : "#333333")
+ onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
+ }
+
+ ToolTip {
+ visible: isInput && iconButton.hovered
+ x: iconButton.x + iconButton.width
+ y: iconButton.y + iconButton.height
+
+ contentItem: Text {
+ text: audio.inputMuted ? qsTr("Click to unmute yourself") : qsTr("Click to mute yourself")
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ background: Rectangle {
+ color: toolTipBackgroundColour
+ radius: 4
+ layer.enabled: true
+ layer.effect: Glow {
+ color: "#66000000"
+ transparentBorder: true
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: inputControls
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 2 * virtualstudio.uiScale
+
+ VolumeSlider {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ labelText: "Send"
+ tooltipText: "How loudly other participants hear you"
+ sliderEnabled: !audio.inputMuted
+ }
+
+ DeviceWarning {
+ id: deviceWarning
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+ }
+ }
+
+ Component {
+ id: outputControls
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 4 * virtualstudio.uiScale
+
+ VolumeSlider {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ labelText: "Studio"
+ tooltipText: "How loudly you hear other participants"
+ sliderEnabled: true
+ }
+
+ VolumeSlider {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ labelText: "Monitor"
+ tooltipText: "How loudly you hear yourself"
+ sliderEnabled: true
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 5 * virtualstudio.uiScale
+
+ Item {
+ Layout.topMargin: 5 * virtualstudio.uiScale
+ Layout.preferredHeight: 30 * virtualstudio.uiScale
+ Layout.fillWidth: true
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 8 * virtualstudio.uiScale
+
+ Item {
+ Layout.fillHeight: true
+ Layout.preferredWidth: 100 * virtualstudio.uiScale
+
+ Loader {
+ id: typeIconIndicator
+ anchors.left: parent.left
+ sourceComponent: controlIndicator
+ }
+
+ Text {
+ id: label
+ anchors.left: parent.left
+ anchors.leftMargin: 36 * virtualstudio.uiScale
+
+ text: isInput ? "Input" : "Output"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ InfoTooltip {
+ content: isInput ? qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") : qsTr("How you'll hear the studio audio")
+ size: 16
+ anchors.left: label.right
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.verticalCenter: label.verticalCenter
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.preferredWidth: 200 * virtualstudio.uiScale
+
+ Meter {
+ anchors.fill: parent
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ model: isInput ? audio.inputMeterLevels : audio.outputMeterLevels
+ clipped: isInput ? audio.inputClipped : audio.outputClipped
+ enabled: true
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.bottomMargin: 5 * virtualstudio.uiScale
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 8 * virtualstudio.uiScale
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.leftMargin: 8 * virtualstudio.uiScale
+ Layout.rightMargin: 8 * virtualstudio.uiScale
+
+ Loader {
+ anchors.fill: parent
+ anchors.top: parent.top
+ sourceComponent: isInput ? inputControls : outputControls
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+ property string disabledButtonText: "#D3D4D4"
+ property string saveButtonText: "#000000"
+ property int fullHeight: 88 * virtualstudio.uiScale
+ property int minimumHeight: 48 * virtualstudio.uiScale
+
+ property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+ property bool isReady: virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+ property bool showDeviceControls: getShowDeviceControls()
+
+ id: deviceControlsGroup
+ width: parent.width
+ height: isReady ? (showDeviceControls ? fullHeight : (feedbackDetectedModal.visible ? minimumHeight : 0)) : minimumHeight;
+ color: backgroundColour
+
+ function getShowDeviceControls () {
+ // self-managed servers do not support minified controls so keep it full size
+ return (!virtualstudio.currentStudio.isManaged && virtualstudio.currentStudio.sessionId === "") || (!virtualstudio.collapseDeviceControls && isReady);
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: false
+ }
+
+ RowLayout {
+ id: layout
+ anchors.fill: parent
+ spacing: 2
+ visible: !feedbackDetectedModal.visible
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ visible: !isReady
+
+ 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.disconnect()
+
+ Text {
+ text: "Back to Studios"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ visible: showDeviceControls
+
+ DeviceControls {
+ isInput: true
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ visible: showDeviceControls
+
+ DeviceControls {
+ isInput: false
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.preferredWidth: 48 * virtualstudio.uiScale
+ visible: showDeviceControls
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 2
+
+ Item {
+ Layout.preferredHeight: 24 * virtualstudio.uiScale
+ Layout.preferredWidth: 24 * virtualstudio.uiScale
+ Layout.topMargin: 2 * virtualstudio.uiScale
+ Layout.rightMargin: 2 * virtualstudio.uiScale
+ Layout.alignment: Qt.AlignRight | Qt.AlignTop
+
+ Button {
+ id: closeDeviceControlsButton
+ visible: virtualstudio.currentStudio.isManaged || virtualstudio.currentStudio.sessionId !== ""
+ width: 24 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ background: Rectangle {
+ color: backgroundColour
+ }
+ anchors.top: parent.top
+ anchors.right: parent.right
+ onClicked: {
+ virtualstudio.collapseDeviceControls = true;
+ }
+
+ AppIcon {
+ id: closeDeviceControlsIcon
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ width: 24 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ color: closeDeviceControlsButton.hovered ? textColour : browserButtonHoverColour
+ icon.source: "close.svg"
+ onClicked: {
+ virtualstudio.collapseDeviceControls = true;
+ }
+ }
+ }
+ }
+
+ Item {
+ visible: isUsingRtAudio
+ Layout.preferredWidth: 40 * virtualstudio.uiScale
+ Layout.preferredHeight: 64 * virtualstudio.uiScale
+ Layout.bottomMargin: 5 * virtualstudio.uiScale
+ Layout.topMargin: 2 * virtualstudio.uiScale
+ Layout.rightMargin: 2 * virtualstudio.uiScale
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+
+ Button {
+ id: changeDevicesButton
+ width: 36 * virtualstudio.uiScale
+ height: 36 * virtualstudio.uiScale
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ background: Rectangle {
+ radius: 8 * virtualstudio.uiScale
+ color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour)
+ }
+ onClicked: {
+ virtualstudio.windowState = "change_devices"
+ if (!audio.deviceModelsInitialized) {
+ audio.refreshDevices();
+ }
+ }
+
+ AppIcon {
+ id: changeDevicesIcon
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ width: 20 * virtualstudio.uiScale
+ height: 20 * virtualstudio.uiScale
+ icon.source: "cog.svg"
+ onClicked: {
+ virtualstudio.windowState = "change_devices"
+ if (!audio.deviceModelsInitialized) {
+ audio.refreshDevices();
+ }
+ }
+ }
+ }
+
+ Text {
+ anchors.top: changeDevicesButton.bottom
+ text: "Devices"
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale}
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: backgroundBorder
+ width: parent.width
+ height: 1
+ anchors.top: layout.top
+ color: strokeColor
+ }
+
+ Popup {
+ id: feedbackDetectedModal
+ padding: 1
+ width: parent.width
+ height: parent.height
+ anchors.centerIn: parent
+ dim: false
+ modal: false
+ focus: true
+ closePolicy: Popup.NoAutoClose
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ border.width: 1
+ border.color: buttonStroke
+ clip: true
+ }
+
+ contentItem: Rectangle {
+ width: parent.width
+ height: 232 * virtualstudio.uiScale
+ color: backgroundColour
+
+ Item {
+ id: feedbackDetectedContent
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ anchors.right: parent.right
+
+ AppIcon {
+ id: feedbackWarningIcon
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.topMargin: 10 * virtualstudio.uiScale
+ width: 32 * virtualstudio.uiScale
+ height: 32 * virtualstudio.uiScale
+ icon.source: "warning.svg"
+ color: "#F21B1B"
+ visible: showDeviceControls
+ }
+
+ AppIcon {
+ id: feedbackWarningIconMinified
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ height: 24 * virtualstudio.uiScale
+ width: 24 * virtualstudio.uiScale
+ icon.source: "warning.svg"
+ color: "#F21B1B"
+ visible: !showDeviceControls
+ }
+
+ Text {
+ id: feedbackDetectedHeader
+ anchors.top: parent.top
+ anchors.topMargin: 10 * virtualstudio.uiScale
+ anchors.left: feedbackWarningIcon.right
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ width: parent.width
+ text: "Audio feedback detected!"
+ font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ visible: showDeviceControls
+ }
+
+ Text {
+ id: feedbackDetectedText
+ anchors.top: feedbackDetectedHeader.bottom
+ anchors.topMargin: 4 * virtualstudio.uiScale
+ anchors.left: feedbackWarningIcon.right
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ width: parent.width
+ text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
+ font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ visible: showDeviceControls
+ }
+
+ Text {
+ id: feedbackDetectedTextMinified
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: feedbackWarningIcon.right
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ width: parent.width
+ text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
+ font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ visible: !showDeviceControls
+ }
+
+ Text {
+ id: feedbackDetectedText2
+ anchors.top: feedbackDetectedText.bottom
+ anchors.topMargin: 2 * virtualstudio.uiScale
+ anchors.left: feedbackWarningIcon.right
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ width: parent.width
+ text: "You can disable this behavior under <b>Settings</b> > <b>Advanced</b>"
+ textFormat: Text.RichText
+ font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ visible: showDeviceControls
+ }
+
+ Button {
+ id: closeFeedbackDetectedModalButton
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ width: 128 * virtualstudio.uiScale;
+ height: 30 * virtualstudio.uiScale
+ onClicked: feedbackDetectedModal.close()
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
+ border.width: 1
+ border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
+ }
+
+ Text {
+ text: "Ok"
+ font.family: "Poppins"
+ font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ visible: showDeviceControls
+ }
+
+ Button {
+ id: closeFeedbackDetectedModalButtonMinified
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ width: 80 * virtualstudio.uiScale
+ height: 24 * virtualstudio.uiScale
+ onClicked: feedbackDetectedModal.close()
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
+ border.width: 1
+ border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
+ }
+
+ Text {
+ text: "Ok"
+ font.family: "Poppins"
+ font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ visible: !showDeviceControls
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: virtualstudio
+
+ function onFeedbackDetected() {
+ if (virtualstudio.windowState === "connected") {
+ feedbackDetectedModal.visible = true;
+ }
+ }
+
+ function onCollapseDeviceControlsChanged(collapseDeviceControls) {
+ showDeviceControls = getShowDeviceControls()
+ }
+
+ function onCurrentStudioChanged(currentStudio) {
+ isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+ showDeviceControls = getShowDeviceControls()
+ }
+
+ function onConnectionStateChanged(connectionState) {
+ isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+ showDeviceControls = getShowDeviceControls()
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Button {
+ id: refreshButton
+ text: "Refresh Devices"
+
+ property int fontExtraSmall: 8
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property var onDeviceRefresh: function () { audio.refreshDevices(); };
+
+ width: 144 * virtualstudio.uiScale;
+ height: 30 * virtualstudio.uiScale
+ palette.buttonText: textColour
+ display: AbstractButton.TextBesideIcon
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ icon {
+ source: "refresh.svg";
+ color: textColour;
+ }
+ onClicked: { onDeviceRefresh(); }
+ font {
+ family: "Poppins"
+ pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ height: 28 * virtualstudio.uiScale
+ property string devicesWarningColour: "#F21B1B"
+
+ function getTooltip () {
+ var result = "";
+ if (Boolean(audio.devicesError)) {
+ result = audio.devicesError;
+ if (audio.devicesErrorHelpUrl) {
+ result += " Click for more info."
+ }
+ } else if (Boolean(audio.devicesWarning)) {
+ result = audio.devicesWarning;
+ if (audio.devicesWarningHelpUrl) {
+ result += " Click for more info."
+ }
+ }
+ return result;
+ }
+
+ AppIcon {
+ id: devicesWarningIcon
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.height
+ height: parent.height
+ icon.source: "warning.svg"
+ color: devicesWarningColour
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+
+ Text {
+ id: warningOrErrorText
+ text: Boolean(audio.devicesError) ? "Audio Configuration Error" : "Audio Configuration Warning"
+ anchors.left: devicesWarningIcon.right
+ anchors.leftMargin: 4 * virtualstudio.uiScale
+ anchors.verticalCenter: devicesWarningIcon.verticalCenter
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: devicesWarningColour
+ }
+
+ InfoTooltip {
+ id: devicesWarningTooltip
+ anchors.left: warningOrErrorText.right
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.top: devicesWarningIcon.top
+ content: getTooltip()
+ iconColor: devicesWarningColour
+ size: 16 * virtualstudio.uiScale
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+
+ MouseArea {
+ id: devicesWarningToolTipArea
+ anchors.top: devicesWarningIcon.top
+ anchors.bottom: devicesWarningIcon.bottom
+ anchors.left: devicesWarningIcon.left
+ anchors.right: devicesWarningTooltip.right
+ hoverEnabled: true
+ onEntered: devicesWarningTooltip.showToolTip = true
+ onExited: devicesWarningTooltip.showToolTip = false
+ onClicked: {
+ if (Boolean(audio.devicesError) && audio.devicesErrorHelpUrl !== "") {
+ virtualstudio.openLink(audio.devicesErrorHelpUrl);
+ } else if (Boolean(audio.devicesWarning) && audio.devicesWarningHelpUrl !== "") {
+ virtualstudio.openLink(audio.devicesWarningHelpUrl);
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ anchors.centerIn: parent
+ width: 480 * virtualstudio.uiScale
+
+ property int fontMedium: 12
+ property int fontSmall: 10
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string devicesWarningColour: "#F21B1B"
+
+ Popup {
+ id: deviceWarningPopup
+ padding: 1
+ width: parent.width
+ height: 350 * virtualstudio.uiScale
+ anchors.centerIn: parent
+ modal: true
+ focus: true
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: 6 * virtualstudio.uiScale
+ border.width: 1
+ border.color: buttonStroke
+ clip: true
+ }
+
+ contentItem: Rectangle {
+ width: parent.width
+ height: parent.height
+ color: backgroundColour
+ radius: 6 * virtualstudio.uiScale
+
+ Item {
+ id: deviceWarningPopupContent
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ AppIcon {
+ id: devicesWarningIcon
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 56 * virtualstudio.uiScale
+ height: 56 * virtualstudio.uiScale
+ icon.source: "warning.svg"
+ color: devicesWarningColour
+ }
+
+ Text {
+ id: deviceWarningPopupHeader
+ anchors.top: devicesWarningIcon.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ width: parent.width
+ text: "Audio Configuration Warning"
+ font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: devicesWarningText
+ anchors.top: deviceWarningPopupHeader.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - (32 * virtualstudio.uiScale)
+ text: qsTr(audio.devicesWarning)
+ font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ LearnMoreButton {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: devicesWarningText.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ url: Boolean(audio.devicesErrorHelpUrl) ? audio.devicesErrorHelpUrl : audio.devicesWarningHelpUrl
+ visible: Boolean(audio.devicesErrorHelpUrl) || Boolean(audio.devicesWarningHelpUrl)
+ }
+
+ Button {
+ id: backButton
+ anchors.left: parent.left
+ anchors.leftMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 24 * virtualstudio.uiScale
+ width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ onClicked: () => {
+ deviceWarningPopup.close();
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: backButton.down ? buttonPressedStroke : (backButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: "Back to Settings"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Button {
+ id: connectButton
+ anchors.right: parent.right
+ anchors.rightMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 24 * virtualstudio.uiScale
+ width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ onClicked: () => {
+ deviceWarningPopup.close();
+ audio.stopAudio(true);
+ virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
+ virtualstudio.windowState = "connected";
+ virtualstudio.saveSettings();
+ virtualstudio.joinStudio();
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: connectButton.down ? buttonPressedColour : (connectButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: connectButton.down ? buttonPressedStroke : (connectButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: "Connect to Session"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+
+ function open () {
+ deviceWarningPopup.open();
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property int leftMargin: 16
+ property int fontBig: 28
+ property int fontMedium: 18
+ property int fontSmall: 11
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+ property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7"
+ property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
+ property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
+ property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
+
+ AppIcon {
+ id: ohnoImage
+ y: 60
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 180
+ height: 180
+ icon.source: "sentiment_very_dissatisfied.svg"
+ }
+
+ Text {
+ id: ohnoHeader
+ text: "Oh no!"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ohnoImage.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: ohnoMessage
+ text: virtualstudio.failedMessage || "Unable to process request - please try again later."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 400
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ohnoHeader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Button {
+ id: backButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: backButton.down ? 1 : 0
+ border.color: buttonStroke
+ layer.enabled: !backButton.down
+ }
+ onClicked: { virtualstudio.windowState = "browse" }
+ width: 256 * virtualstudio.uiScale
+ height: 42 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ohnoMessage.bottom
+ anchors.topMargin: 60 * virtualstudio.uiScale
+ Text {
+ text: "Back"
+ font.family: "Poppins"
+ font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour)
+ }
+ visible: true
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ id: userFeedbackSurvey
+
+ anchors.centerIn: parent
+ width: 480 * virtualstudio.uiScale
+ height: 232 * virtualstudio.uiScale
+
+ property int leftHeaderMargin: 16
+ property int fontBig: 28
+ property int fontMedium: 12
+ property int fontSmall: 10
+ property int fontTiny: 8
+ property int bodyMargin: 60
+ property int rightMargin: 16
+ property int bottomToolTipMargin: 8
+ property int rightToolTipMargin: 4
+
+ property string buttonColour: "#F2F3F3"
+ property string buttonHoverColour: "#E7E8E8"
+ property string buttonPressedColour: "#E7E8E8"
+ property string buttonStroke: "#EAEBEB"
+ property string buttonHoverStroke: "#B0B5B5"
+ property string buttonPressedStroke: "#B0B5B5"
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
+ property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+
+ property string serverId: ""
+
+ property int rating: 0
+ property int hover: star1MouseArea.containsMouse ? 1 : star2MouseArea.containsMouse ? 2 : star3MouseArea.containsMouse ? 3 : star4MouseArea.containsMouse ? 4 : star5MouseArea.containsMouse ? 5 : 0
+ property int currentView: (hover > 0 ? hover : rating)
+ property bool submitted: false;
+
+ property string message: ""
+
+ Popup {
+ id: userFeedbackModal
+ padding: 1
+ width: parent.width
+ height: 300 * virtualstudio.uiScale
+ anchors.centerIn: parent
+ modal: true
+ focus: true
+ closePolicy: Popup.NoAutoClose
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: 6 * virtualstudio.uiScale
+ border.width: 1
+ border.color: buttonStroke
+ clip: true
+ }
+
+ contentItem: Rectangle {
+ width: parent.width
+ height: parent.height
+ color: backgroundColour
+ radius: 6 * virtualstudio.uiScale
+
+ Item {
+ id: userFeedbackSurveyContent
+ anchors.top: parent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ visible: !submitted
+
+ Text {
+ id: userFeedbackSurveyHeader
+ anchors.top: parent.top
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ text: "How did your session go?"
+ font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: ratingItemInstructions
+ anchors.top: userFeedbackSurveyHeader.bottom
+ anchors.topMargin: 12 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ text: "Rate your session on a scale of 1 to 5"
+ font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Item {
+ id: ratingItem
+ anchors.top: ratingItemInstructions.bottom
+ anchors.topMargin: 4 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ height: 40 * virtualstudio.uiScale
+ width: 200 * virtualstudio.uiScale
+
+ Item {
+ id: star1Container
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: star2Container.left
+ width: parent.width / 5
+ height: parent.height
+
+ AppIcon {
+ id: star1Icon
+ anchors.centerIn: parent
+ width: currentView >= 1 ? parent.width : 20 * virtualstudio.uiScale
+ height: currentView >= 1 ? parent.height : 20 * virtualstudio.uiScale
+ icon.source: "star.svg"
+ color: currentView >= 1 ? "#faaf00" : "#606060"
+ }
+
+ MouseArea {
+ id: star1MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: () => {
+ if (rating === 1) {
+ rating = 0;
+ } else {
+ rating = 1;
+ }
+ }
+ }
+ }
+
+ Item {
+ id: star2Container
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: star3Container.left
+ width: parent.width / 5
+ height: parent.height
+
+ AppIcon {
+ id: star2Icon
+ anchors.centerIn: parent
+ width: currentView >= 2 ? parent.width : 20 * virtualstudio.uiScale
+ height: currentView >= 2 ? parent.height : 20 * virtualstudio.uiScale
+ icon.source: "star.svg"
+ color: currentView >= 2 ? "#faaf00" : "#606060"
+ }
+
+ MouseArea {
+ id: star2MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: () => {
+ if (rating === 2) {
+ rating = 0;
+ } else {
+ rating = 2;
+ }
+ }
+ }
+ }
+
+ Item {
+ id: star3Container
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width / 5
+ height: parent.height
+
+ AppIcon {
+ id: star3Icon
+ anchors.centerIn: parent
+ width: currentView >= 3 ? parent.width : 20 * virtualstudio.uiScale
+ height: currentView >= 3 ? parent.height : 20 * virtualstudio.uiScale
+ icon.source: "star.svg"
+ color: currentView >= 3 ? "#faaf00" : "#606060"
+ }
+
+ MouseArea {
+ id: star3MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: () => {
+ if (rating === 3) {
+ rating = 0;
+ } else {
+ rating = 3;
+ }
+ }
+ }
+ }
+
+ Item {
+ id: star4Container
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: star3Container.right
+ width: parent.width / 5
+ height: parent.height
+
+ AppIcon {
+ id: star4Icon
+ anchors.centerIn: parent
+ width: currentView >= 4 ? parent.width : 20 * virtualstudio.uiScale
+ height: currentView >= 4 ? parent.height : 20 * virtualstudio.uiScale
+ icon.source: "star.svg"
+ color: currentView >= 4 ? "#faaf00" : "#606060"
+ }
+
+ MouseArea {
+ id: star4MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: () => {
+ if (rating === 4) {
+ rating = 0;
+ } else {
+ rating = 4;
+ }
+ }
+ }
+ }
+
+ Item {
+ id: star5Container
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: star4Container.right
+ width: parent.width / 5
+ height: parent.height
+
+ AppIcon {
+ id: star5Icon
+ anchors.centerIn: parent
+ width: currentView >= 5 ? parent.width : 20 * virtualstudio.uiScale
+ height: currentView >= 5 ? parent.height : 20 * virtualstudio.uiScale
+ icon.source: "star.svg"
+ color: currentView >= 5 ? "#faaf00" : "#606060"
+ }
+
+ MouseArea {
+ id: star5MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: () => {
+ if (rating === 5) {
+ rating = 0;
+ } else {
+ rating = 5;
+ }
+ }
+ }
+ }
+ }
+
+ ScrollView {
+ id: messageBoxScrollArea
+ anchors.left: parent.left
+ anchors.leftMargin: 32 * virtualstudio.uiScale
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.top: ratingItem.bottom
+ anchors.topMargin: 12 * virtualstudio.uiScale
+ height: 64 * virtualstudio.uiScale
+
+ TextArea {
+ id: messageBox
+ placeholderText: qsTr("(Optional) Let us know how we can improve your experience.")
+ placeholderTextColor: textAreaTextColour
+ color: textColour
+ background: Rectangle {
+ color: textAreaColour
+ radius: 6 * virtualstudio.uiScale
+ border.width: 1
+ border.color: buttonStroke
+ }
+ }
+ }
+
+ Item {
+ id: buttonsArea
+ height: 32 * virtualstudio.uiScale
+ width: 324 * virtualstudio.uiScale
+ anchors.horizontalCenter: messageBoxScrollArea.horizontalCenter
+ anchors.top: messageBoxScrollArea.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+
+ Button {
+ id: userFeedbackButton
+ anchors.right: buttonsArea.right
+ anchors.horizontalCenter: buttonsArea.horizontalCenter
+ anchors.verticalCenter: parent.buttonsArea
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ onClicked: () => {
+ if (rating === 0 && messageBox.text === "") {
+ userFeedbackModal.close();
+ serverId = "";
+ messageBox.clear();
+ return;
+ }
+ virtualstudio.collectFeedbackSurvey(serverId, rating, messageBox.text);
+ submitted = true;
+ rating = 0;
+ serverId = "";
+ messageBox.clear();
+ userFeedbackModal.height = 150 * virtualstudio.uiScale
+ submittedFeedbackTimer.start();
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: "#000000"
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Timer {
+ id: submittedFeedbackTimer
+ interval: 5000; running: false; repeat: false
+ onTriggered: () => {
+ submitted = false;
+ userFeedbackModal.height = 300 * virtualstudio.uiScale
+ userFeedbackModal.close();
+ }
+ }
+ }
+ }
+
+ Item {
+ id: submittedFeedbackContent
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ visible: submitted
+
+ Text {
+ id: submittedFeedbackHeader
+ anchors.top: submittedFeedbackContent.top
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ text: "Thank you!"
+ font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Text {
+ id: submittedFeedbackText
+ anchors.top: submittedFeedbackHeader.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ text: "Your feedback has been recorded."
+ font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ Button {
+ id: closeButtonFeedback
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: submittedFeedbackText.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ onClicked: () => {
+ submitted = false;
+ userFeedbackModal.height = 300 * virtualstudio.uiScale
+ userFeedbackModal.close();
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: "Close"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: virtualstudio
+
+ function onOpenFeedbackSurveyModal(serverId) {
+ userFeedbackSurvey.serverId = serverId;
+ userFeedbackModal.open();
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+ property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+ property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+
+ Image {
+ id: jtlogo
+ source: "logo.svg"
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 35 * virtualstudio.uiScale
+ width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale
+ sourceSize: Qt.size(jtlogo.width,jtlogo.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ Text {
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 168 * virtualstudio.uiScale
+ text: "Sign in with a Virtual Studio account?"
+ font.family: "Poppins"
+ font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale
+ color: textColour
+ }
+
+ Text {
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 219 * virtualstudio.uiScale
+ text: "You'll be able to change your mind later"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ color: textColour
+ }
+
+ Button {
+ id: vsButton
+ background: Rectangle {
+ radius: 10 * virtualstudio.uiScale
+ color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke)
+ layer.enabled: vsButton.hovered && !vsButton.down
+ }
+ onClicked: { virtualstudio.toVirtualStudioMode(); }
+ x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+ width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+ Text {
+ text: "Yes"
+ font.family: "Poppins"
+ font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: "#000000"
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ Text {
+ text: "• Seamless Audio & Video<br>• Recording & Livestreaming<br>• No Servers Required"
+ textFormat: Text.StyledText
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ x: parent.width / 2 - (265 * virtualstudio.uiScale);
+ y: 355 * virtualstudio.uiScale;
+ width: 230 * virtualstudio.uiScale
+ padding: 0
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ }
+ Image {
+ source: "JTVS.png"
+ x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+ width: 234 * virtualstudio.uiScale; height: 195 * virtualstudio.uiScale;
+ }
+
+ Button {
+ id: standardButton
+ background: Rectangle {
+ radius: 10 * virtualstudio.uiScale
+ color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke)
+ layer.enabled: standardButton.hovered && !standardButton.down
+ }
+ onClicked: { virtualstudio.toClassicMode(); }
+ x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+ width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+ Text {
+ text: "No"
+ font.family: "Poppins"
+ font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: "#000000"
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ Image {
+ source: "JTOriginal.png"
+ x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+ width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale;
+ }
+ Text {
+ text: virtualstudio.psiBuild ? "• Connect via IP address<br>• Run a local hub server<br>• The Standard JackTrip experience" :
+ "• Connect via IP address<br>• Run a local hub server<br>• The Classic JackTrip experience"
+ textFormat: Text.StyledText
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ x: parent.width / 2 + (32 * virtualstudio.uiScale);
+ y: 355 * virtualstudio.uiScale;
+ width: 230 * virtualstudio.uiScale
+ padding: 0
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ color: textColour
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+ id: footer
+ width: parent.width
+ height: 24
+ anchors.bottom: parent.bottom
+ color: backgroundColour
+ clip: true
+
+ property string statsOrange: "#b26a00"
+ property string connectionStateColor: getConnectionStateColor()
+ property variant networkStatsText: getNetworkStatsText()
+
+ function getConnectionStateColor() {
+ if (virtualstudio.connectionState == "Connected") {
+ return meterGreen
+ }
+ if (virtualstudio.connectionState.includes("Disconnected") || virtualstudio.connectionState.includes("Error")) {
+ return meterRed
+ }
+ if (studioStatus === "Starting" || virtualstudio.connectionState == "Connecting..." || virtualstudio.connectionState == "Reconnecting...") {
+ return meterYellow
+ }
+ return "grey"
+ }
+
+ function getNetworkStatsText() {
+ let minRtt = virtualstudio.networkStats.minRtt;
+ let maxRtt = virtualstudio.networkStats.maxRtt;
+ let avgRtt = virtualstudio.networkStats.avgRtt;
+
+ let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed];
+ if (virtualstudio.networkOutage) {
+ return texts;
+ }
+
+ texts = ["Measuring...", "", "grey"];
+ if (!minRtt || !maxRtt) {
+ return texts;
+ }
+
+ texts[1] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms";
+ let quality = "Poor";
+ let color = meterRed;
+ if (avgRtt < 10 && maxRtt < 15) {
+ quality = "Excellent";
+ color = meterGreen;
+ } else if (avgRtt < 20 && maxRtt < 30) {
+ quality = "Good";
+ color = meterYellow;
+ } else if (avgRtt < 30 && maxRtt < 40) {
+ quality = "Fair";
+ color = statsOrange;
+ }
+
+ texts[0] = quality
+ texts[2] = color;
+ return texts;
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: false
+ }
+
+ RowLayout {
+ id: layout
+ anchors.fill: parent
+ spacing: 4
+
+ Rectangle {
+ color: backgroundColour
+ Layout.minimumWidth: 256
+ Layout.preferredWidth: 512
+ Layout.maximumWidth: 640
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ visible: studioStatus === "Ready"
+
+ AppIcon {
+ id: connectionQualityIcon
+ anchors.left: parent.left
+ anchors.leftMargin: 8 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ width: 20 * virtualstudio.uiScale
+ height: 20 * virtualstudio.uiScale
+ icon.source: "speed.svg"
+ }
+
+ Text {
+ id: connectionQualityText
+ anchors.left: connectionQualityIcon.right
+ anchors.leftMargin: 4 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Connection:"
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Text {
+ id: connectionQualityName
+ anchors.left: connectionQualityText.right
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ text: networkStatsText[0]
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: networkStatsText[2]
+ }
+
+ Text {
+ id: connectionQualityTime
+ anchors.left: connectionQualityName.right
+ anchors.leftMargin: 8 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ text: networkStatsText[1]
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ Rectangle {
+ color: backgroundColour
+ Layout.minimumWidth: 96
+ Layout.preferredWidth: 128
+ Layout.maximumWidth: 160
+ Layout.fillHeight: true
+
+ Rectangle {
+ id: connectionStatusDot
+ anchors.right: connectionStatusText.left
+ anchors.rightMargin: 4 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ width: 12
+ height: connectionStatusDot.width
+ radius: connectionStatusDot.height / 2
+ color: connectionStateColor
+ }
+
+ Text {
+ id: connectionStatusText
+ anchors.right: parent.right
+ anchors.rightMargin: 8 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ text: studioStatus === "Starting" ? "Starting..." : virtualstudio.connectionState
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+ }
+ }
+
+ Rectangle {
+ id: backgroundBorder
+ width: parent.width
+ height: 1
+ y: parent.height - footer.height
+ color: buttonStroke
+ }
+
+ Connections {
+ target: virtualstudio
+
+ function onConnectionStateChanged() {
+ connectionStatusDot.color = getConnectionStateColor()
+ }
+ function onNetworkStatsChanged() {
+ networkStatsText = getNetworkStatsText();
+ }
+ function onUpdatedNetworkOutage() {
+ networkStatsText = getNetworkStatsText();
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+Item {
+ required property string content
+ required property int size
+
+ width: size * virtualstudio.uiScale
+ height: size * virtualstudio.uiScale
+
+ property string iconSource: "help.svg"
+ property string iconColor: ""
+ property string backgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+ property bool showToolTip: false
+
+ Item {
+ anchors.fill: parent
+
+ AppIcon {
+ id: tooltipIcon
+ anchors.centerIn: parent
+ width: parent.width
+ height: parent.height
+ icon.source: iconSource
+ color: iconColor
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: tooltipIcon
+ hoverEnabled: true
+ onEntered: showToolTip = true
+ onExited: showToolTip = false
+ }
+
+ ToolTip {
+ visible: showToolTip
+ x: tooltipIcon.x + tooltipIcon.width
+ y: tooltipIcon.y + tooltipIcon.height
+
+ contentItem: Text {
+ text: content
+ font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ elide: Text.ElideRight
+ wrapMode: Text.WordWrap
+ }
+
+ background: Rectangle {
+ color: backgroundColour
+ radius: 4
+ layer.enabled: true
+ layer.effect: Glow {
+ radius: 8
+ color: "#66000000"
+ transparentBorder: true
+ }
+ }
+ }
+ }
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 JTApplication.h
+ * \author Matt Hortoon
+ * \date July 2022
+ */
+
+#ifndef JTAPPLICATION_H
+#define JTAPPLICATION_H
+
+#include <QApplication>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QEvent>
+#include <QFileOpenEvent>
+#include <QObject>
+
+class JTApplication : public QApplication
+{
+ Q_OBJECT
+
+ public:
+ JTApplication(int& argc, char** argv) : QApplication(argc, argv) {}
+
+ bool event(QEvent* event) override
+ {
+ if (event->type() == QEvent::FileOpen) {
+ QFileOpenEvent* openEvent = static_cast<QFileOpenEvent*>(event);
+
+ QDesktopServices::openUrl(openEvent->url());
+ }
+ return QApplication::event(event);
+ }
+};
+
+#endif // JTAPPLICATION_H
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Button {
+ property string url
+ property string buttonText: "Learn more"
+
+ width: 150 * virtualstudio.uiScale;
+ height: 30 * virtualstudio.uiScale
+
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+
+ onClicked: {
+ virtualstudio.openLink(url);
+ }
+
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke)
+ }
+
+ Text {
+ text: buttonText
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import VS 1.0
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ state: auth.authenticationStage
+ states: [
+ State {
+ name: "unauthenticated"
+ },
+ State {
+ name: "refreshing"
+ },
+ State {
+ name: "polling"
+ },
+ State {
+ name: "success"
+ },
+ State {
+ name: "failed"
+ }
+ ]
+
+ Rectangle {
+ width: parent.width; height: parent.height
+ color: backgroundColour
+ }
+
+ property bool codeCopied: false
+ property int numFailures: 0;
+
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+ property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
+ property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
+ property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
+ property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+ property string linkTextColour: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+ property string toolTipTextColour: codeCopied ? "#FAFBFB" : textColour
+ property string toolTipBackgroundColour: codeCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3")
+ property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string disabledButtonText: "#D3D4D4"
+ property string errorTextColour: "#DB0A0A"
+
+ property bool showCodeFlow: (loginScreen.state === "unauthenticated" && !auth.attemptingRefreshToken) || (loginScreen.state === "polling" || loginScreen.state === "failed" || (loginScreen.state === "success" && auth.authenticationMethod === "code flow"))
+ property bool showLoading: (loginScreen.state === "unauthenticated" ** auth.attemptingRefreshToken) || loginScreen.state === "refreshing" || (loginScreen.state === "success" && auth.authenticationMethod === "refresh token")
+
+ Clipboard {
+ id: clipboard
+ }
+
+ function getVerificationCodeText() {
+ if (Boolean(auth.verificationCode)) {
+ return auth.verificationCode;
+ }
+ if (numFailures < 5) {
+ return "Loading...";
+ }
+ var result;
+ if (auth.errorMessage.startsWith("Host") && auth.errorMessage.endsWith("not found")) {
+ result = "Your Internet connection is offline.";
+ } else {
+ result = "There was an error trying to sign in.";
+ }
+ result += "<br/> Please try again.";
+ return result;
+ }
+
+ Item {
+ id: loginScreenHeader
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: showCodeFlow ? 48 * virtualstudio.uiScale : 144 * virtualstudio.uiScale
+
+ Image {
+ id: loginLogo
+ source: "logo.svg"
+ x: parent.width / 2 - (150 * virtualstudio.uiScale);
+ width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale
+ sourceSize: Qt.size(loginLogo.width,loginLogo.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ Image {
+ source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
+ anchors.bottom: loginLogo.bottom
+ x: parent.width / 2 - (88 * virtualstudio.uiScale)
+ width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale
+ }
+
+ Text {
+ text: "Virtual Studio"
+ font.family: "Poppins"
+ font.pixelSize: 24 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 80 * virtualstudio.uiScale
+ color: textColour
+ }
+ }
+
+ Item {
+ id: codeFlow
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 68 * virtualstudio.uiScale
+ height: parent.height - codeFlow.y
+ visible: showCodeFlow
+ width: parent.width
+
+ Text {
+ id: deviceVerificationExplanation
+ text: `To get started, please sign in and confirm the following code using your web browser. Return here when you are done.`
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 128 * virtualstudio.uiScale
+ width: 500 * virtualstudio.uiScale;
+ visible: Boolean(auth.verificationCode)
+ color: textColour
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ textFormat: Text.RichText
+ onLinkActivated: link => {
+ if (!Boolean(auth.verificationCode)) {
+ return;
+ }
+ virtualstudio.openLink(link)
+ }
+ }
+
+ AppIcon {
+ id: successIcon
+ y: 224 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 96 * virtualstudio.uiScale
+ height: 96 * virtualstudio.uiScale
+ icon.source: "check.svg"
+ color: "green"
+ visible: loginScreen.state === "success"
+ }
+
+ Text {
+ id: deviceVerificationCode
+ text: getVerificationCodeText()
+ font.family: "Poppins"
+ font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.letterSpacing: Boolean(auth.verificationCode) ? 8 : 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 196 * virtualstudio.uiScale
+ width: 540 * virtualstudio.uiScale;
+ visible: !auth.isAuthenticated
+ color: Boolean(auth.verificationCode) ? textColour : disabledButtonText
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+
+ Timer {
+ id: copiedResetTimer
+ interval: 2000; running: false; repeat: false
+ onTriggered: codeCopied = false;
+ }
+
+ MouseArea {
+ id: deviceVerificationCodeMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ enabled: Boolean(auth.verificationCode)
+ hoverEnabled: true
+ onClicked: () => {
+ codeCopied = true;
+ clipboard.setText(auth.verificationCode);
+ copiedResetTimer.restart()
+ }
+ }
+
+ ToolTip {
+ parent: deviceVerificationCode
+ visible: loginScreen.state === "polling" && deviceVerificationCodeMouseArea.containsMouse
+ delay: 100
+ contentItem: Rectangle {
+ color: toolTipBackgroundColour
+ radius: 3
+ anchors.fill: parent
+ layer.enabled: true
+ border.width: 1
+ border.color: tooltipStroke
+
+ Text {
+ anchors.centerIn: parent
+ font { family: "Poppins"; pixelSize: 8 * virtualstudio.fontScale * virtualstudio.uiScale}
+ text: codeCopied ? qsTr("📋 Copied code to clipboard") : qsTr("📋 Copy code to Clipboard")
+ color: toolTipTextColour
+ }
+ }
+ background: Rectangle {
+ color: "transparent"
+ }
+ }
+ }
+
+ Button {
+ id: loginButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : buttonStroke)
+ layer.enabled: !loginButton.down
+ }
+ onClicked: {
+ if (auth.verificationCode && auth.verificationUrl) {
+ virtualstudio.openLink(auth.verificationUrl);
+ }
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 260 * virtualstudio.uiScale
+ width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+ Text {
+ text: "Sign In"
+ font.family: "Poppins"
+ font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour)
+ }
+ visible: !auth.isAuthenticated && Boolean(auth.verificationCode)
+ }
+
+ Text {
+ id: authFailedText
+ text: auth.errorMessage
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: loginScreenFooter.top
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ visible: (loginScreen.state === "failed" && numFailures >= 5) && loginScreen.state !== "success"
+ color: errorTextColour
+ }
+
+ Item {
+ id: loginScreenFooter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 24 * virtualstudio.uiScale
+ width: parent.width
+ height: 48 * virtualstudio.uiScale
+
+ property bool showBackButton: !virtualstudio.vsFtux
+
+ Item {
+ id: backButton
+ visible: parent.showBackButton
+ anchors.verticalCenter: parent.verticalCenter
+ x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale
+ width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+ Text {
+ text: "Back"
+ font.family: "Poppins"
+ font.underline: true
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: textColour
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: () => { if (!auth.isAuthenticated) { virtualstudio.windowState = "start"; } }
+ cursorShape: Qt.PointingHandCursor
+ }
+ }
+
+ Item {
+ id: resetCodeButton
+ visible: loginScreen.state == "failed" || (!auth.isAuthenticated && auth.verificationCode)
+ x: parent.showBackButton ? (parent.x + parent.width / 2) + 8 * virtualstudio.uiScale : (parent.x + parent.width / 2) - resetCodeButton.width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+ Text {
+ text: auth.verificationCode ? "Reset Code" : "Retry"
+ font.family: "Poppins"
+ font.underline: true
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: textColour
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: () => {
+ if (auth.verificationCode) {
+ auth.resetCode();
+ } else {
+ numFailures = 0;
+ virtualstudio.login();
+ }
+ }
+ cursorShape: Qt.PointingHandCursor
+ }
+ }
+ }
+ }
+
+ Item {
+ id: refreshToken
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 108 * virtualstudio.uiScale
+ visible: showLoading
+
+ Text {
+ id: loadingViaRefreshToken
+ text: "Logging In...";
+ font.family: "Poppins"
+ font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 208 * virtualstudio.uiScale
+ width: 360 * virtualstudio.uiScale;
+ color: textColour
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Timer {
+ id: retryTimer
+ running: false
+ repeat: false
+ interval: 300
+ onTriggered: { virtualstudio.login(); }
+ }
+
+ Connections {
+ target: auth
+ function onUpdatedAuthenticationStage (stage) {
+ loginScreen.state = stage;
+ if (stage === "failed") {
+ numFailures = numFailures + 1;
+ if (numFailures < 5) {
+ retryTimer.restart();
+ }
+ }
+ if (stage === "success") {
+ numFailures = 0;
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ required property var model
+ property int bins: Math.max(15, width/20)
+ property int innerMargin: 2 * virtualstudio.uiScale
+ property int boxRadius: 3 * virtualstudio.uiScale
+ property int boxThickness: 12
+ required property bool clipped
+ property bool enabled: true
+ property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
+ property string meterRed: "#F21B1B"
+
+ Item {
+ id: meters
+ width: parent.width - boxThickness - innerMargin
+ height: parent.height
+
+ MeterBars {
+ id: leftchannel
+ x: 0
+ y: 0
+ width: parent.width
+ height: boxThickness
+ level: parent.parent.model[0]
+ enabled: parent.parent.enabled
+ }
+
+ MeterBars {
+ id: rightchannel
+ x: 0;
+ anchors.top: leftchannel.bottom
+ anchors.topMargin: innerMargin
+ width: parent.width
+ height: boxThickness
+ level: parent.parent.model[1]
+ enabled: parent.parent.enabled
+ }
+ }
+
+ Rectangle {
+ id: clipIndicator
+ y: 0
+ anchors.left: meters.right
+ anchors.leftMargin: innerMargin
+
+ width: boxThickness
+ height: leftchannel.height + rightchannel.height + innerMargin
+ radius: boxRadius
+ color: clipped ? meterRed : meterColor
+ }
+}
\ No newline at end of file
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+ required property var level
+ required property var enabled
+ property int boxHeight: height
+ property int boxWidth: (width / bins) - innerMargin
+ property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
+ property string meterGreen: "#61C554"
+ property string meterYellow: "#F5BF4F"
+ property string meterRed: "#F21B1B"
+
+ function getBoxColor (idx) {
+ // Case where the meter should not be filled
+ if (!enabled || level <= (idx / bins)) {
+ return meterColor;
+ }
+ // Case where the meter should be filled
+ let fillColor = meterGreen;
+ if (idx > 0.5*bins && idx <= 0.8*bins) {
+ fillColor = meterYellow;
+ } else if (idx > 0.8*bins) {
+ fillColor = meterRed;
+ }
+ return fillColor;
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: innerMargin
+
+ Repeater {
+ model: bins
+ Rectangle {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ x: (boxWidth) * index + innerMargin * index;
+ y: 0
+ z: 1
+ width: boxWidth
+ height: boxHeight
+ color: getBoxColor(index)
+ radius: boxRadius
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property int fontBig: 20
+ property int fontMedium: 13
+ property int fontSmall: 11
+ property int fontExtraSmall: 8
+
+ property string saveButtonPressedColour: "#E7E8E8"
+ property string saveButtonPressedStroke: "#B0B5B5"
+ property string saveButtonBackgroundColour: "#F2F3F3"
+ property string saveButtonStroke: "#EAEBEB"
+ property string saveButtonText: "#000000"
+
+ Item {
+ id: requestMicPermissionsItem
+ width: parent.width; height: parent.height
+ visible: permissions.micPermission == "unknown"
+
+ AppIcon {
+ id: microphonePrompt
+ y: 60
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 260
+ height: 250
+ icon.source: "Prompt.svg"
+ }
+
+ Image {
+ id: micLogo
+ source: "logo.svg"
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: microphonePrompt.top
+ anchors.topMargin: 18 * virtualstudio.uiScale
+ width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+ sourceSize: Qt.size(micLogo.width,micLogo.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ Button {
+ id: showPromptButton
+ width: 112 * virtualstudio.uiScale
+ height: 30 * virtualstudio.uiScale
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 2
+ border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: showPromptButton.hovered && !showPromptButton.down
+ }
+ onClicked: {
+ permissions.getMicPermission();
+ }
+ anchors.right: microphonePrompt.right
+ anchors.rightMargin: 13.5 * virtualstudio.uiScale
+ anchors.bottomMargin: 17 * virtualstudio.uiScale
+ anchors.bottom: microphonePrompt.bottom
+ Text {
+ text: "OK"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Text {
+ id: micPermissionsHeader
+ text: "JackTrip needs your sounds!"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: microphonePrompt.bottom
+ anchors.topMargin: 48 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: micPermissionsSubheader1
+ text: "JackTrip requires permission to use your microphone."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 400
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: micPermissionsHeader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: micPermissionsSubheader2
+ text: "Click ‘OK’ to give JackTrip access to your microphone, instrument, or other audio device."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 400
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: micPermissionsSubheader1.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ }
+ }
+
+ Item {
+ id: noMicItem
+ width: parent.width; height: parent.height
+ visible: permissions.micPermission == "denied"
+
+ AppIcon {
+ id: noMic
+ y: 60
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 109.27
+ height: 170
+ icon.source: "micoff.svg"
+ }
+
+ Button {
+ id: openSettingsButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: openSettingsButton.hovered && !openSettingsButton.down
+ }
+ onClicked: {
+ permissions.openSystemPrivacy();
+ }
+ anchors.right: parent.right
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Open Privacy Settings"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Text {
+ id: noMicHeader
+ text: "JackTrip can't hear you!"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: noMic.bottom
+ anchors.topMargin: 48 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: noMicSubheader1
+ text: "JackTrip requires permission to use your microphone."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 400
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: noMicHeader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: noMicSubheader2
+ text: "Click 'Open Privacy Settings' to give JackTrip permission to access your microphone, instrument, or other audio device."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 400
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: noMicSubheader1.bottom
+ anchors.topMargin: 24 * virtualstudio.uiScale
+ }
+ }
+
+ Connections {
+ target: permissions
+
+ function onMicPermissionUpdated() {
+ if (permissions.micPermission === "granted") {
+ if (virtualstudio.studioToJoin === "") {
+ virtualstudio.windowState = "browse";
+ } else {
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
+ }
+ }
+ }
+ }
+
+}
--- /dev/null
+<svg width="260" height="250" viewBox="0 0 260 250" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="1" y="1" width="258" height="248" rx="9" stroke="#0F0D0D" stroke-width="2"/>
+<rect x="18" y="205" width="108" height="26" rx="5" stroke="#0F0D0D" stroke-width="2"/>
+<path d="M42.1189 222H44.9021C47.4861 222 48.9158 220.4 48.9158 217.775V217.764C48.9158 215.145 47.4802 213.545 44.9021 213.545H42.1189V222ZM42.9216 221.273V214.271H44.8552C46.9177 214.271 48.0955 215.59 48.0955 217.77V217.781C48.0955 219.961 46.9236 221.273 44.8552 221.273H42.9216ZM53.151 222.105C54.9147 222.105 56.0221 220.857 56.0221 218.848V218.836C56.0221 216.826 54.9147 215.584 53.151 215.584C51.3873 215.584 50.2857 216.826 50.2857 218.836V218.848C50.2857 220.857 51.3873 222.105 53.151 222.105ZM53.151 221.414C51.8502 221.414 51.0768 220.441 51.0768 218.848V218.836C51.0768 217.242 51.8502 216.275 53.151 216.275C54.4518 216.275 55.2252 217.242 55.2252 218.836V218.848C55.2252 220.441 54.4518 221.414 53.151 221.414ZM57.6323 222H58.4057V218.25C58.4057 217.061 59.1616 216.275 60.2749 216.275C61.3706 216.275 61.9155 216.891 61.9155 218.092V222H62.6889V217.898C62.6889 216.428 61.8862 215.584 60.4799 215.584C59.4956 215.584 58.8159 216.035 58.4819 216.797H58.4057V215.689H57.6323V222ZM64.598 216.662H65.1898L66.0335 213.545H65.1312L64.598 216.662ZM70.0871 222.041C70.2746 222.041 70.4503 222.023 70.6261 221.994V221.326C70.4679 221.344 70.3507 221.35 70.175 221.35C69.4601 221.35 69.1847 221.027 69.1847 220.254V216.34H70.6261V215.689H69.1847V214.049H68.382V215.689H67.3683V216.34H68.382V220.43C68.382 221.578 68.88 222.041 70.0871 222.041ZM74.8251 222H75.6688L76.6063 219.41H80.1629L81.1004 222H81.9442L78.7801 213.545H77.9833L74.8251 222ZM78.3465 214.594H78.4168L79.911 218.719H76.8524L78.3465 214.594ZM83.3845 222H84.1579V213.176H83.3845V222ZM86.2076 222H86.981V213.176H86.2076V222ZM91.5209 222.105C93.2846 222.105 94.392 220.857 94.392 218.848V218.836C94.392 216.826 93.2846 215.584 91.5209 215.584C89.7572 215.584 88.6557 216.826 88.6557 218.836V218.848C88.6557 220.857 89.7572 222.105 91.5209 222.105ZM91.5209 221.414C90.2201 221.414 89.4467 220.441 89.4467 218.848V218.836C89.4467 217.242 90.2201 216.275 91.5209 216.275C92.8217 216.275 93.5951 217.242 93.5951 218.836V218.848C93.5951 220.441 92.8217 221.414 91.5209 221.414ZM96.969 222H97.7776L99.3303 216.762H99.4065L100.959 222H101.774L103.573 215.689H102.799L101.387 221.045H101.317L99.758 215.689H98.9905L97.4319 221.045H97.3616L95.9612 215.689H95.1819L96.969 222Z" fill="#272525"/>
+<path d="M141.5 205H239.5C242.261 205 244.5 207.239 244.5 210V226C244.5 228.761 242.261 231 239.5 231H141.5C138.739 231 136.5 228.761 136.5 226V210C136.5 207.239 138.739 205 141.5 205Z" fill="white" stroke="#0F0D0D" stroke-width="2"/>
+<path d="M185.77 213.328C183.286 213.328 181.698 215.027 181.698 217.77C181.698 220.512 183.262 222.217 185.77 222.217C188.272 222.217 189.831 220.512 189.831 217.77C189.831 215.033 188.266 213.328 185.77 213.328ZM185.77 214.881C187.147 214.881 188.026 216 188.026 217.77C188.026 219.533 187.147 220.664 185.77 220.664C184.381 220.664 183.508 219.533 183.508 217.77C183.508 216 184.399 214.881 185.77 214.881ZM193.228 222V219.451L194.066 218.461L196.521 222H198.642L195.343 217.254L198.425 213.545H196.456L193.333 217.312H193.228V213.545H191.458V222H193.228Z" fill="#272525"/>
+<path d="M20.1581 104.84L18.9647 108.814H20.7801L21.5164 104.84H20.1581ZM23.0716 104.84L21.9735 108.814H23.7889L24.43 104.84H23.0716ZM24.6637 111.309C24.6637 113.105 25.8951 114.235 27.774 114.235C29.7164 114.235 30.8844 113.13 30.8844 111.156V104.84H28.9674V111.144C28.9674 112.045 28.5357 112.521 27.7486 112.521C27.0123 112.521 26.5299 112.045 26.5172 111.309H24.6637ZM35.3011 112.718C34.66 112.718 34.2093 112.4 34.2093 111.88C34.2093 111.378 34.5965 111.093 35.39 111.036L36.8055 110.947V111.461C36.8055 112.172 36.1581 112.718 35.3011 112.718ZM34.6917 114.108C35.5995 114.108 36.3612 113.727 36.723 113.086H36.8373V114H38.6273V109.22C38.6273 107.722 37.5799 106.846 35.7264 106.846C33.9681 106.846 32.7811 107.659 32.6605 108.954H34.3617C34.514 108.535 34.9583 108.306 35.6249 108.306C36.3866 108.306 36.8055 108.636 36.8055 109.22V109.792L35.1107 109.893C33.3714 109.995 32.4002 110.731 32.4002 112C32.4002 113.283 33.346 114.108 34.6917 114.108ZM46.586 109.468C46.4337 107.875 45.3229 106.846 43.4693 106.846C41.2794 106.846 40.0479 108.16 40.0479 110.483C40.0479 112.832 41.2857 114.152 43.4693 114.152C45.2911 114.152 46.4337 113.143 46.586 111.575H44.8595C44.7198 112.267 44.2247 112.635 43.4693 112.635C42.4791 112.635 41.9142 111.88 41.9142 110.483C41.9142 109.106 42.4728 108.363 43.4693 108.363C44.2564 108.363 44.7325 108.801 44.8595 109.468H46.586ZM50.0062 109.734H49.8919V104.326H48.0448V114H49.8919V111.677L50.3998 111.156L52.4437 114H54.6336L51.7835 110.001L54.4496 106.999H52.3485L50.0062 109.734ZM59.7169 114V106.478H62.4654V104.84H55.0514V106.478H57.7999V114H59.7169ZM63.1878 114H65.035V110.122C65.035 109.138 65.7269 108.541 66.7362 108.541C67.0345 108.541 67.4662 108.598 67.6122 108.655V106.973C67.4535 106.916 67.1424 106.884 66.8885 106.884C65.9999 106.884 65.2762 107.417 65.0921 108.116H64.9779V106.999H63.1878V114ZM68.8107 114H70.6578V106.999H68.8107V114ZM69.7374 106.021C70.3976 106.021 70.8292 105.634 70.8292 105.088C70.8292 104.536 70.3976 104.155 69.7374 104.155C69.0709 104.155 68.6456 104.536 68.6456 105.088C68.6456 105.634 69.0709 106.021 69.7374 106.021ZM76.5218 106.884C75.557 106.884 74.7635 107.36 74.3827 108.147H74.2684V106.999H72.4784V116.323H74.3255V112.927H74.4398C74.7826 113.67 75.5506 114.108 76.5536 114.108C78.3055 114.108 79.3783 112.756 79.3783 110.496C79.3783 108.23 78.2928 106.884 76.5218 106.884ZM75.8934 112.565C74.9159 112.565 74.3065 111.785 74.3065 110.502C74.3065 109.22 74.9159 108.433 75.8998 108.433C76.8837 108.433 77.4803 109.214 77.4803 110.496C77.4803 111.791 76.89 112.565 75.8934 112.565ZM82.2145 108.814L83.3063 104.84H81.4972L80.8497 108.814H82.2145ZM85.128 108.814L86.3151 104.84H84.506L83.7633 108.814H85.128ZM100.95 106.999H99.1158L98.0748 112.02H97.9606L96.7291 106.999H94.9645L93.7394 112.02H93.6251L92.5841 106.999H90.7115L92.5778 114H94.5201L95.7579 109.169H95.8722L97.1227 114H99.0904L100.95 106.999ZM105.031 114.152C107.182 114.152 108.477 112.788 108.477 110.496C108.477 108.224 107.163 106.846 105.031 106.846C102.898 106.846 101.584 108.23 101.584 110.496C101.584 112.788 102.879 114.152 105.031 114.152ZM105.031 112.642C104.04 112.642 103.482 111.861 103.482 110.496C103.482 109.15 104.047 108.357 105.031 108.357C106.008 108.357 106.579 109.15 106.579 110.496C106.579 111.854 106.015 112.642 105.031 112.642ZM116.271 106.999H114.424V111.036C114.424 111.969 113.929 112.546 113.008 112.546C112.158 112.546 111.72 112.051 111.72 111.086V106.999H109.873V111.562C109.873 113.188 110.812 114.152 112.323 114.152C113.383 114.152 114.037 113.689 114.367 112.876H114.481V114H116.271V106.999ZM118.136 114H119.983V104.326H118.136V114ZM124.362 114.108C125.333 114.108 126.127 113.657 126.501 112.902H126.615V114H128.405V104.326H126.558V108.116H126.45C126.089 107.341 125.308 106.884 124.362 106.884C122.604 106.884 121.512 108.262 121.512 110.49C121.512 112.724 122.597 114.108 124.362 114.108ZM124.99 108.433C125.974 108.433 126.577 109.227 126.577 110.502C126.577 111.785 125.981 112.565 124.99 112.565C124 112.565 123.41 111.791 123.41 110.496C123.41 109.214 124.006 108.433 124.99 108.433ZM133.633 114H135.481V104.326H133.633V114ZM137.365 114H139.212V106.999H137.365V114ZM138.291 106.021C138.952 106.021 139.383 105.634 139.383 105.088C139.383 104.536 138.952 104.155 138.291 104.155C137.625 104.155 137.2 104.536 137.2 105.088C137.2 105.634 137.625 106.021 138.291 106.021ZM143.057 109.734H142.943V104.326H141.096V114H142.943V111.677L143.451 111.156L145.495 114H147.685L144.835 110.001L147.501 106.999H145.4L143.057 109.734ZM151.41 108.262C152.273 108.262 152.831 108.839 152.87 109.766H149.886C149.95 108.858 150.553 108.262 151.41 108.262ZM152.908 112.007C152.711 112.47 152.209 112.73 151.479 112.73C150.515 112.73 149.905 112.083 149.88 111.042V110.947H154.685V110.382C154.685 108.16 153.466 106.846 151.403 106.846C149.321 106.846 148.039 108.255 148.039 110.534C148.039 112.807 149.296 114.152 151.429 114.152C153.142 114.152 154.349 113.327 154.628 112.007H152.908ZM159.976 105.335V107.068H158.885V108.522H159.976V112.108C159.976 113.473 160.649 114.025 162.35 114.025C162.706 114.025 163.049 113.987 163.277 113.943V112.534C163.1 112.553 162.973 112.559 162.731 112.559C162.103 112.559 161.824 112.28 161.824 111.67V108.522H163.277V107.068H161.824V105.335H159.976ZM167.821 114.152C169.973 114.152 171.268 112.788 171.268 110.496C171.268 108.224 169.954 106.846 167.821 106.846C165.688 106.846 164.374 108.23 164.374 110.496C164.374 112.788 165.669 114.152 167.821 114.152ZM167.821 112.642C166.831 112.642 166.272 111.861 166.272 110.496C166.272 109.15 166.837 108.357 167.821 108.357C168.798 108.357 169.37 109.15 169.37 110.496C169.37 111.854 168.805 112.642 167.821 112.642ZM178.552 112.718C177.911 112.718 177.461 112.4 177.461 111.88C177.461 111.378 177.848 111.093 178.641 111.036L180.057 110.947V111.461C180.057 112.172 179.409 112.718 178.552 112.718ZM177.943 114.108C178.851 114.108 179.612 113.727 179.974 113.086H180.089V114H181.879V109.22C181.879 107.722 180.831 106.846 178.978 106.846C177.219 106.846 176.032 107.659 175.912 108.954H177.613C177.765 108.535 178.21 108.306 178.876 108.306C179.638 108.306 180.057 108.636 180.057 109.22V109.792L178.362 109.893C176.623 109.995 175.652 110.731 175.652 112C175.652 113.283 176.597 114.108 177.943 114.108ZM189.837 109.468C189.685 107.875 188.574 106.846 186.721 106.846C184.531 106.846 183.299 108.16 183.299 110.483C183.299 112.832 184.537 114.152 186.721 114.152C188.542 114.152 189.685 113.143 189.837 111.575H188.111C187.971 112.267 187.476 112.635 186.721 112.635C185.73 112.635 185.165 111.88 185.165 110.483C185.165 109.106 185.724 108.363 186.721 108.363C187.508 108.363 187.984 108.801 188.111 109.468H189.837ZM197.428 109.468C197.276 107.875 196.165 106.846 194.311 106.846C192.121 106.846 190.89 108.16 190.89 110.483C190.89 112.832 192.128 114.152 194.311 114.152C196.133 114.152 197.276 113.143 197.428 111.575H195.701C195.562 112.267 195.067 112.635 194.311 112.635C193.321 112.635 192.756 111.88 192.756 110.483C192.756 109.106 193.315 108.363 194.311 108.363C195.098 108.363 195.574 108.801 195.701 109.468H197.428ZM201.851 108.262C202.714 108.262 203.273 108.839 203.311 109.766H200.328C200.391 108.858 200.994 108.262 201.851 108.262ZM203.349 112.007C203.152 112.47 202.651 112.73 201.921 112.73C200.956 112.73 200.347 112.083 200.321 111.042V110.947H205.126V110.382C205.126 108.16 203.908 106.846 201.845 106.846C199.763 106.846 198.48 108.255 198.48 110.534C198.48 112.807 199.737 114.152 201.87 114.152C203.584 114.152 204.79 113.327 205.069 112.007H203.349ZM206.433 109.055C206.433 110.134 207.093 110.782 208.451 111.08L209.721 111.366C210.337 111.499 210.603 111.708 210.603 112.051C210.603 112.502 210.108 112.8 209.391 112.8C208.654 112.8 208.204 112.527 208.064 112.051H206.261C206.388 113.397 207.506 114.152 209.353 114.152C211.187 114.152 212.438 113.238 212.438 111.842C212.438 110.794 211.828 110.223 210.47 109.925L209.156 109.639C208.508 109.493 208.21 109.284 208.21 108.941C208.21 108.497 208.699 108.198 209.359 108.198C210.045 108.198 210.47 108.478 210.565 108.928H212.273C212.171 107.589 211.124 106.846 209.346 106.846C207.601 106.846 206.433 107.729 206.433 109.055ZM213.617 109.055C213.617 110.134 214.277 110.782 215.636 111.08L216.905 111.366C217.521 111.499 217.788 111.708 217.788 112.051C217.788 112.502 217.292 112.8 216.575 112.8C215.839 112.8 215.388 112.527 215.248 112.051H213.446C213.573 113.397 214.69 114.152 216.537 114.152C218.371 114.152 219.622 113.238 219.622 111.842C219.622 110.794 219.013 110.223 217.654 109.925L216.34 109.639C215.693 109.493 215.394 109.284 215.394 108.941C215.394 108.497 215.883 108.198 216.543 108.198C217.229 108.198 217.654 108.478 217.749 108.928H219.457C219.355 107.589 218.308 106.846 216.531 106.846C214.785 106.846 213.617 107.729 213.617 109.055ZM224.913 105.335V107.068H223.822V108.522H224.913V112.108C224.913 113.473 225.586 114.025 227.288 114.025C227.643 114.025 227.986 113.987 228.214 113.943V112.534C228.037 112.553 227.91 112.559 227.668 112.559C227.04 112.559 226.761 112.28 226.761 111.67V108.522H228.214V107.068H226.761V105.335H224.913ZM229.768 114H231.615V109.982C231.615 109.074 232.149 108.458 233.075 108.458C233.913 108.458 234.364 108.96 234.364 109.931V114H236.211V109.493C236.211 107.811 235.316 106.865 233.812 106.865C232.783 106.865 232.022 107.348 231.698 108.147H231.584V104.326H229.768V114ZM240.983 108.262C241.847 108.262 242.405 108.839 242.443 109.766H239.46C239.523 108.858 240.126 108.262 240.983 108.262ZM242.481 112.007C242.285 112.47 241.783 112.73 241.053 112.73C240.088 112.73 239.479 112.083 239.454 111.042V110.947H244.259V110.382C244.259 108.16 243.04 106.846 240.977 106.846C238.895 106.846 237.613 108.255 237.613 110.534C237.613 112.807 238.87 114.152 241.002 114.152C242.716 114.152 243.922 113.327 244.202 112.007H242.481ZM91.5539 130H93.4011V125.785C93.4011 125.004 93.9153 124.439 94.6389 124.439C95.3625 124.439 95.7942 124.865 95.7942 125.607V130H97.5715V125.715C97.5715 124.973 98.0539 124.439 98.803 124.439C99.5837 124.439 99.9709 124.852 99.9709 125.684V130H101.818V125.195C101.818 123.754 100.948 122.846 99.552 122.846C98.5744 122.846 97.7683 123.36 97.4446 124.141H97.3303C97.051 123.329 96.3782 122.846 95.3943 122.846C94.4739 122.846 93.7439 123.341 93.4582 124.141H93.344V122.999H91.5539V130ZM103.575 130H105.422V122.999H103.575V130ZM104.502 122.021C105.162 122.021 105.594 121.634 105.594 121.088C105.594 120.536 105.162 120.155 104.502 120.155C103.835 120.155 103.41 120.536 103.41 121.088C103.41 121.634 103.835 122.021 104.502 122.021ZM113.438 125.468C113.286 123.875 112.175 122.846 110.322 122.846C108.132 122.846 106.9 124.16 106.9 126.483C106.9 128.832 108.138 130.152 110.322 130.152C112.143 130.152 113.286 129.143 113.438 127.575H111.712C111.572 128.267 111.077 128.635 110.322 128.635C109.331 128.635 108.766 127.88 108.766 126.483C108.766 125.106 109.325 124.363 110.322 124.363C111.109 124.363 111.585 124.801 111.712 125.468H113.438ZM114.833 130H116.681V126.122C116.681 125.138 117.373 124.541 118.382 124.541C118.68 124.541 119.112 124.598 119.258 124.655V122.973C119.099 122.916 118.788 122.884 118.534 122.884C117.646 122.884 116.922 123.417 116.738 124.116H116.624V122.999H114.833V130ZM123.383 130.152C125.534 130.152 126.829 128.788 126.829 126.496C126.829 124.224 125.515 122.846 123.383 122.846C121.25 122.846 119.936 124.23 119.936 126.496C119.936 128.788 121.231 130.152 123.383 130.152ZM123.383 128.642C122.392 128.642 121.834 127.861 121.834 126.496C121.834 125.15 122.399 124.357 123.383 124.357C124.36 124.357 124.931 125.15 124.931 126.496C124.931 127.854 124.366 128.642 123.383 128.642ZM132.332 122.884C131.367 122.884 130.573 123.36 130.192 124.147H130.078V122.999H128.288V132.323H130.135V128.927H130.25C130.592 129.67 131.36 130.108 132.363 130.108C134.115 130.108 135.188 128.756 135.188 126.496C135.188 124.23 134.103 122.884 132.332 122.884ZM131.703 128.565C130.726 128.565 130.116 127.785 130.116 126.502C130.116 125.22 130.726 124.433 131.709 124.433C132.693 124.433 133.29 125.214 133.29 126.496C133.29 127.791 132.7 128.565 131.703 128.565ZM136.704 130H138.551V125.982C138.551 125.074 139.084 124.458 140.011 124.458C140.849 124.458 141.3 124.96 141.3 125.931V130H143.147V125.493C143.147 123.811 142.252 122.865 140.747 122.865C139.719 122.865 138.957 123.348 138.634 124.147H138.519V120.326H136.704V130ZM147.995 130.152C150.147 130.152 151.442 128.788 151.442 126.496C151.442 124.224 150.128 122.846 147.995 122.846C145.862 122.846 144.548 124.23 144.548 126.496C144.548 128.788 145.843 130.152 147.995 130.152ZM147.995 128.642C147.005 128.642 146.446 127.861 146.446 126.496C146.446 125.15 147.011 124.357 147.995 124.357C148.973 124.357 149.544 125.15 149.544 126.496C149.544 127.854 148.979 128.642 147.995 128.642ZM152.901 130H154.748V125.963C154.748 125.042 155.275 124.439 156.132 124.439C157.008 124.439 157.42 124.947 157.42 125.912V130H159.267V125.474C159.267 123.798 158.429 122.846 156.874 122.846C155.84 122.846 155.129 123.335 154.805 124.122H154.691V122.999H152.901V130ZM164.04 124.262C164.903 124.262 165.461 124.839 165.5 125.766H162.516C162.58 124.858 163.183 124.262 164.04 124.262ZM165.538 128.007C165.341 128.47 164.839 128.73 164.109 128.73C163.145 128.73 162.535 128.083 162.51 127.042V126.947H167.315V126.382C167.315 124.16 166.096 122.846 164.033 122.846C161.951 122.846 160.669 124.255 160.669 126.534C160.669 128.807 161.926 130.152 164.059 130.152C165.772 130.152 166.979 129.327 167.258 128.007H165.538ZM170.113 130.171C170.799 130.171 171.237 129.708 171.237 129.067C171.237 128.426 170.799 127.969 170.113 127.969C169.434 127.969 168.99 128.426 168.99 129.067C168.99 129.708 169.434 130.171 170.113 130.171Z" fill="#272525"/>
+<path d="M23.7968 160V152.494H26.5214V151.545H20.0175V152.494H22.7421V160H23.7968ZM28.032 160H29.0398V156.262C29.0398 155.195 29.6609 154.48 30.7917 154.48C31.7468 154.48 32.2507 155.037 32.2507 156.156V160H33.2585V155.91C33.2585 154.428 32.4148 153.572 31.0788 153.572C30.112 153.572 29.4499 153.982 29.1335 154.68H29.0398V151.176H28.032V160ZM35.0738 160H36.0816V153.684H35.0738V160ZM35.5777 152.4C35.9644 152.4 36.2808 152.084 36.2808 151.697C36.2808 151.311 35.9644 150.994 35.5777 150.994C35.191 150.994 34.8746 151.311 34.8746 151.697C34.8746 152.084 35.191 152.4 35.5777 152.4ZM37.809 155.412C37.809 156.326 38.3481 156.836 39.5317 157.123L40.6157 157.387C41.2895 157.551 41.6176 157.844 41.6176 158.277C41.6176 158.857 41.0082 159.262 40.1586 159.262C39.35 159.262 38.8461 158.922 38.6762 158.389H37.6391C37.7504 159.438 38.7172 160.111 40.1235 160.111C41.559 160.111 42.6547 159.332 42.6547 158.201C42.6547 157.293 42.0805 156.777 40.8911 156.49L39.9184 156.256C39.1743 156.074 38.8227 155.805 38.8227 155.371C38.8227 154.809 39.4086 154.428 40.1586 154.428C40.9203 154.428 41.4125 154.762 41.5473 155.266H42.5434C42.4086 154.229 41.4887 153.572 40.1645 153.572C38.8227 153.572 37.809 154.363 37.809 155.412ZM49.4728 159.227C48.7404 159.227 48.1954 158.852 48.1954 158.207C48.1954 157.574 48.6173 157.24 49.5783 157.176L51.2775 157.064V157.645C51.2775 158.547 50.5099 159.227 49.4728 159.227ZM49.2853 160.111C50.129 160.111 50.8204 159.742 51.2306 159.068H51.3243V160H52.2853V155.676C52.2853 154.363 51.424 153.572 49.8829 153.572C48.5353 153.572 47.5392 154.24 47.4044 155.254H48.424C48.5646 154.756 49.0919 154.469 49.8478 154.469C50.7911 154.469 51.2775 154.896 51.2775 155.676V156.25L49.4552 156.361C47.9845 156.449 47.1525 157.1 47.1525 158.23C47.1525 159.385 48.0607 160.111 49.2853 160.111ZM57.1767 153.572C56.3154 153.572 55.5596 154.012 55.1553 154.738H55.0615V153.684H54.1006V162.109H55.1084V159.051H55.2021C55.5478 159.719 56.2744 160.111 57.1767 160.111C58.7822 160.111 59.831 158.816 59.831 156.842C59.831 154.855 58.7881 153.572 57.1767 153.572ZM56.9365 159.203C55.7998 159.203 55.0791 158.289 55.0791 156.842C55.0791 155.389 55.7998 154.48 56.9424 154.48C58.0967 154.48 58.7881 155.365 58.7881 156.842C58.7881 158.318 58.0967 159.203 56.9365 159.203ZM64.4412 153.572C63.5799 153.572 62.8241 154.012 62.4198 154.738H62.326V153.684H61.3651V162.109H62.3729V159.051H62.4666C62.8123 159.719 63.5389 160.111 64.4412 160.111C66.0467 160.111 67.0955 158.816 67.0955 156.842C67.0955 154.855 66.0526 153.572 64.4412 153.572ZM64.201 159.203C63.0643 159.203 62.3436 158.289 62.3436 156.842C62.3436 155.389 63.0643 154.48 64.2069 154.48C65.3612 154.48 66.0526 155.365 66.0526 156.842C66.0526 158.318 65.3612 159.203 64.201 159.203ZM71.9683 160H72.9761V156.086C72.9761 155.195 73.6734 154.551 74.6343 154.551C74.8335 154.551 75.1968 154.586 75.2788 154.609V153.602C75.1499 153.584 74.939 153.572 74.7749 153.572C73.937 153.572 73.2105 154.006 73.023 154.621H72.9292V153.684H71.9683V160ZM78.8343 154.463C79.8363 154.463 80.5043 155.201 80.5277 156.32H77.0472C77.1234 155.201 77.8265 154.463 78.8343 154.463ZM80.4984 158.365C80.2347 158.922 79.684 159.221 78.8695 159.221C77.7972 159.221 77.1 158.43 77.0472 157.182V157.135H81.5883V156.748C81.5883 154.785 80.5511 153.572 78.8461 153.572C77.1117 153.572 75.9984 154.861 75.9984 156.848C75.9984 158.846 77.0941 160.111 78.8461 160.111C80.2289 160.111 81.2015 159.449 81.5062 158.365H80.4984ZM85.4485 153.572C83.8488 153.572 82.8059 154.861 82.8059 156.842C82.8059 158.834 83.8371 160.111 85.4485 160.111C86.3391 160.111 87.0188 159.742 87.4231 159.051H87.5168V162.109H88.5363V153.684H87.5637V154.738H87.4699C87.0949 154.029 86.3098 153.572 85.4485 153.572ZM85.677 159.203C84.5285 159.203 83.8488 158.324 83.8488 156.842C83.8488 155.365 84.5344 154.48 85.6828 154.48C86.8254 154.48 87.5461 155.395 87.5461 156.842C87.5461 158.295 86.8313 159.203 85.677 159.203ZM95.4962 153.684H94.4883V157.422C94.4883 158.529 93.879 159.191 92.7657 159.191C91.7579 159.191 91.336 158.664 91.336 157.527V153.684H90.3282V157.773C90.3282 159.268 91.0665 160.111 92.4844 160.111C93.4512 160.111 94.1251 159.713 94.4415 159.01H94.5352V160H95.4962V153.684ZM97.37 160H98.3778V153.684H97.37V160ZM97.8739 152.4C98.2607 152.4 98.5771 152.084 98.5771 151.697C98.5771 151.311 98.2607 150.994 97.8739 150.994C97.4872 150.994 97.1708 151.311 97.1708 151.697C97.1708 152.084 97.4872 152.4 97.8739 152.4ZM100.252 160H101.26V156.086C101.26 155.195 101.957 154.551 102.918 154.551C103.117 154.551 103.48 154.586 103.562 154.609V153.602C103.433 153.584 103.222 153.572 103.058 153.572C102.22 153.572 101.494 154.006 101.306 154.621H101.213V153.684H100.252V160ZM107.118 154.463C108.12 154.463 108.788 155.201 108.811 156.32H105.331C105.407 155.201 106.11 154.463 107.118 154.463ZM108.782 158.365C108.518 158.922 107.967 159.221 107.153 159.221C106.081 159.221 105.383 158.43 105.331 157.182V157.135H109.872V156.748C109.872 154.785 108.835 153.572 107.13 153.572C105.395 153.572 104.282 154.861 104.282 156.848C104.282 158.846 105.378 160.111 107.13 160.111C108.512 160.111 109.485 159.449 109.79 158.365H108.782ZM111.259 155.412C111.259 156.326 111.798 156.836 112.982 157.123L114.066 157.387C114.74 157.551 115.068 157.844 115.068 158.277C115.068 158.857 114.458 159.262 113.609 159.262C112.8 159.262 112.296 158.922 112.126 158.389H111.089C111.201 159.438 112.167 160.111 113.574 160.111C115.009 160.111 116.105 159.332 116.105 158.201C116.105 157.293 115.531 156.777 114.341 156.49L113.369 156.256C112.624 156.074 112.273 155.805 112.273 155.371C112.273 154.809 112.859 154.428 113.609 154.428C114.371 154.428 114.863 154.762 114.998 155.266H115.994C115.859 154.229 114.939 153.572 113.615 153.572C112.273 153.572 111.259 154.363 111.259 155.412ZM120.978 160H121.986V156.086C121.986 155.195 122.624 154.48 123.45 154.48C124.247 154.48 124.769 154.961 124.769 155.711V160H125.777V155.939C125.777 155.137 126.362 154.48 127.241 154.48C128.132 154.48 128.571 154.938 128.571 155.869V160H129.579V155.635C129.579 154.311 128.859 153.572 127.569 153.572C126.696 153.572 125.976 154.012 125.636 154.68H125.542C125.249 154.023 124.652 153.572 123.796 153.572C122.952 153.572 122.319 153.977 122.032 154.68H121.939V153.684H120.978V160ZM131.395 160H132.402V153.684H131.395V160ZM131.898 152.4C132.285 152.4 132.602 152.084 132.602 151.697C132.602 151.311 132.285 150.994 131.898 150.994C131.512 150.994 131.195 151.311 131.195 151.697C131.195 152.084 131.512 152.4 131.898 152.4ZM139.503 155.617C139.327 154.492 138.39 153.572 136.854 153.572C135.085 153.572 133.96 154.85 133.96 156.818C133.96 158.828 135.091 160.111 136.86 160.111C138.378 160.111 139.321 159.256 139.503 158.096H138.483C138.296 158.811 137.704 159.203 136.854 159.203C135.729 159.203 135.003 158.277 135.003 156.818C135.003 155.389 135.718 154.48 136.854 154.48C137.763 154.48 138.319 154.99 138.483 155.617H139.503ZM140.943 160H141.951V156.086C141.951 155.195 142.648 154.551 143.609 154.551C143.808 154.551 144.172 154.586 144.254 154.609V153.602C144.125 153.584 143.914 153.572 143.75 153.572C142.912 153.572 142.185 154.006 141.998 154.621H141.904V153.684H140.943V160ZM147.885 160.111C149.684 160.111 150.797 158.869 150.797 156.842C150.797 154.809 149.684 153.572 147.885 153.572C146.086 153.572 144.973 154.809 144.973 156.842C144.973 158.869 146.086 160.111 147.885 160.111ZM147.885 159.203C146.69 159.203 146.016 158.336 146.016 156.842C146.016 155.342 146.69 154.48 147.885 154.48C149.081 154.48 149.754 155.342 149.754 156.842C149.754 158.336 149.081 159.203 147.885 159.203ZM155.408 153.572C154.546 153.572 153.79 154.012 153.386 154.738H153.292V153.684H152.331V162.109H153.339V159.051H153.433C153.779 159.719 154.505 160.111 155.408 160.111C157.013 160.111 158.062 158.816 158.062 156.842C158.062 154.855 157.019 153.572 155.408 153.572ZM155.167 159.203C154.031 159.203 153.31 158.289 153.31 156.842C153.31 155.389 154.031 154.48 155.173 154.48C156.328 154.48 157.019 155.365 157.019 156.842C157.019 158.318 156.328 159.203 155.167 159.203ZM159.655 160H160.662V156.262C160.662 155.195 161.283 154.48 162.414 154.48C163.369 154.48 163.873 155.037 163.873 156.156V160H164.881V155.91C164.881 154.428 164.037 153.572 162.701 153.572C161.735 153.572 161.073 153.982 160.756 154.68H160.662V151.176H159.655V160ZM169.269 160.111C171.067 160.111 172.181 158.869 172.181 156.842C172.181 154.809 171.067 153.572 169.269 153.572C167.47 153.572 166.357 154.809 166.357 156.842C166.357 158.869 167.47 160.111 169.269 160.111ZM169.269 159.203C168.073 159.203 167.4 158.336 167.4 156.842C167.4 155.342 168.073 154.48 169.269 154.48C170.464 154.48 171.138 155.342 171.138 156.842C171.138 158.336 170.464 159.203 169.269 159.203ZM173.715 160H174.723V156.262C174.723 155.154 175.373 154.48 176.381 154.48C177.389 154.48 177.869 155.02 177.869 156.156V160H178.877V155.91C178.877 154.41 178.086 153.572 176.668 153.572C175.701 153.572 175.086 153.982 174.769 154.68H174.676V153.684H173.715V160ZM183.194 154.463C184.196 154.463 184.864 155.201 184.887 156.32H181.407C181.483 155.201 182.186 154.463 183.194 154.463ZM184.858 158.365C184.595 158.922 184.044 159.221 183.229 159.221C182.157 159.221 181.46 158.43 181.407 157.182V157.135H185.948V156.748C185.948 154.785 184.911 153.572 183.206 153.572C181.471 153.572 180.358 154.861 180.358 156.848C180.358 158.846 181.454 160.111 183.206 160.111C184.589 160.111 185.561 159.449 185.866 158.365H184.858ZM192.766 159.227C192.034 159.227 191.489 158.852 191.489 158.207C191.489 157.574 191.911 157.24 192.872 157.176L194.571 157.064V157.645C194.571 158.547 193.803 159.227 192.766 159.227ZM192.579 160.111C193.422 160.111 194.114 159.742 194.524 159.068H194.618V160H195.579V155.676C195.579 154.363 194.717 153.572 193.176 153.572C191.829 153.572 190.833 154.24 190.698 155.254H191.717C191.858 154.756 192.385 154.469 193.141 154.469C194.084 154.469 194.571 154.896 194.571 155.676V156.25L192.749 156.361C191.278 156.449 190.446 157.1 190.446 158.23C190.446 159.385 191.354 160.111 192.579 160.111ZM202.62 155.617C202.445 154.492 201.507 153.572 199.972 153.572C198.202 153.572 197.077 154.85 197.077 156.818C197.077 158.828 198.208 160.111 199.978 160.111C201.495 160.111 202.439 159.256 202.62 158.096H201.601C201.413 158.811 200.822 159.203 199.972 159.203C198.847 159.203 198.12 158.277 198.12 156.818C198.12 155.389 198.835 154.48 199.972 154.48C200.88 154.48 201.437 154.99 201.601 155.617H202.62ZM209.287 155.617C209.111 154.492 208.174 153.572 206.639 153.572C204.869 153.572 203.744 154.85 203.744 156.818C203.744 158.828 204.875 160.111 206.645 160.111C208.162 160.111 209.106 159.256 209.287 158.096H208.268C208.08 158.811 207.488 159.203 206.639 159.203C205.514 159.203 204.787 158.277 204.787 156.818C204.787 155.389 205.502 154.48 206.639 154.48C207.547 154.48 208.104 154.99 208.268 155.617H209.287ZM213.247 154.463C214.249 154.463 214.917 155.201 214.94 156.32H211.46C211.536 155.201 212.239 154.463 213.247 154.463ZM214.911 158.365C214.647 158.922 214.097 159.221 213.282 159.221C212.21 159.221 211.513 158.43 211.46 157.182V157.135H216.001V156.748C216.001 154.785 214.964 153.572 213.259 153.572C211.524 153.572 210.411 154.861 210.411 156.848C210.411 158.846 211.507 160.111 213.259 160.111C214.642 160.111 215.614 159.449 215.919 158.365H214.911ZM217.389 155.412C217.389 156.326 217.928 156.836 219.111 157.123L220.195 157.387C220.869 157.551 221.197 157.844 221.197 158.277C221.197 158.857 220.588 159.262 219.738 159.262C218.93 159.262 218.426 158.922 218.256 158.389H217.219C217.33 159.438 218.297 160.111 219.703 160.111C221.139 160.111 222.234 159.332 222.234 158.201C222.234 157.293 221.66 156.777 220.471 156.49L219.498 156.256C218.754 156.074 218.402 155.805 218.402 155.371C218.402 154.809 218.988 154.428 219.738 154.428C220.5 154.428 220.992 154.762 221.127 155.266H222.123C221.988 154.229 221.068 153.572 219.744 153.572C218.402 153.572 217.389 154.363 217.389 155.412ZM223.505 155.412C223.505 156.326 224.044 156.836 225.227 157.123L226.311 157.387C226.985 157.551 227.313 157.844 227.313 158.277C227.313 158.857 226.704 159.262 225.854 159.262C225.046 159.262 224.542 158.922 224.372 158.389H223.335C223.446 159.438 224.413 160.111 225.819 160.111C227.255 160.111 228.35 159.332 228.35 158.201C228.35 157.293 227.776 156.777 226.587 156.49L225.614 156.256C224.87 156.074 224.518 155.805 224.518 155.371C224.518 154.809 225.104 154.428 225.854 154.428C226.616 154.428 227.108 154.762 227.243 155.266H228.239C228.104 154.229 227.184 153.572 225.86 153.572C224.518 153.572 223.505 154.363 223.505 155.412ZM233.645 152.049V153.684H232.625V154.527H233.645V158.359C233.645 159.566 234.166 160.047 235.467 160.047C235.666 160.047 235.86 160.023 236.059 159.988V159.139C235.872 159.156 235.772 159.162 235.59 159.162C234.934 159.162 234.653 158.846 234.653 158.102V154.527H236.059V153.684H234.653V152.049H233.645ZM240.036 160.111C241.835 160.111 242.949 158.869 242.949 156.842C242.949 154.809 241.835 153.572 240.036 153.572C238.238 153.572 237.124 154.809 237.124 156.842C237.124 158.869 238.238 160.111 240.036 160.111ZM240.036 159.203C238.841 159.203 238.167 158.336 238.167 156.842C238.167 155.342 238.841 154.48 240.036 154.48C241.232 154.48 241.906 155.342 241.906 156.842C241.906 158.336 241.232 159.203 240.036 159.203ZM27.91 173.227C27.1776 173.227 26.6327 172.852 26.6327 172.207C26.6327 171.574 27.0546 171.24 28.0155 171.176L29.7147 171.064V171.645C29.7147 172.547 28.9472 173.227 27.91 173.227ZM27.7225 174.111C28.5663 174.111 29.2577 173.742 29.6679 173.068H29.7616V174H30.7225V169.676C30.7225 168.363 29.8612 167.572 28.3202 167.572C26.9725 167.572 25.9765 168.24 25.8417 169.254H26.8612C27.0018 168.756 27.5292 168.469 28.285 168.469C29.2284 168.469 29.7147 168.896 29.7147 169.676V170.25L27.8925 170.361C26.4218 170.449 25.5897 171.1 25.5897 172.23C25.5897 173.385 26.4979 174.111 27.7225 174.111ZM32.5964 174H33.6042V165.176H32.5964V174ZM35.5719 174H36.5797V165.176H35.5719V174ZM41.0844 174.111C42.8832 174.111 43.9965 172.869 43.9965 170.842C43.9965 168.809 42.8832 167.572 41.0844 167.572C39.2856 167.572 38.1723 168.809 38.1723 170.842C38.1723 172.869 39.2856 174.111 41.0844 174.111ZM41.0844 173.203C39.8891 173.203 39.2153 172.336 39.2153 170.842C39.2153 169.342 39.8891 168.48 41.0844 168.48C42.2797 168.48 42.9535 169.342 42.9535 170.842C42.9535 172.336 42.2797 173.203 41.0844 173.203ZM53.2415 167.684H52.2278L50.9856 172.734H50.8919L49.4798 167.684H48.513L47.1009 172.734H47.0071L45.7649 167.684H44.7454L46.5149 174H47.5345L48.9407 169.113H49.0345L50.4466 174H51.472L53.2415 167.684ZM58.3017 166.049V167.684H57.2822V168.527H58.3017V172.359C58.3017 173.566 58.8232 174.047 60.124 174.047C60.3232 174.047 60.5166 174.023 60.7158 173.988V173.139C60.5283 173.156 60.4287 173.162 60.247 173.162C59.5908 173.162 59.3095 172.846 59.3095 172.102V168.527H60.7158V167.684H59.3095V166.049H58.3017ZM62.2498 174H63.2576V170.262C63.2576 169.195 63.8787 168.48 65.0096 168.48C65.9647 168.48 66.4686 169.037 66.4686 170.156V174H67.4764V169.91C67.4764 168.428 66.6326 167.572 65.2967 167.572C64.3299 167.572 63.6678 167.982 63.3514 168.68H63.2576V165.176H62.2498V174ZM71.7878 168.463C72.7897 168.463 73.4577 169.201 73.4811 170.32H70.0007C70.0768 169.201 70.78 168.463 71.7878 168.463ZM73.4518 172.365C73.1882 172.922 72.6374 173.221 71.8229 173.221C70.7507 173.221 70.0534 172.43 70.0007 171.182V171.135H74.5417V170.748C74.5417 168.785 73.5046 167.572 71.7995 167.572C70.0651 167.572 68.9518 168.861 68.9518 170.848C68.9518 172.846 70.0475 174.111 71.7995 174.111C73.1823 174.111 74.155 173.449 74.4596 172.365H73.4518ZM80.4457 167.684H79.4379V174.328C79.4379 175.025 79.2035 175.307 78.4886 175.307H78.3363V176.168H78.5121C79.8363 176.168 80.4457 175.611 80.4457 174.311V167.684ZM79.9418 166.4C80.3285 166.4 80.6449 166.084 80.6449 165.697C80.6449 165.311 80.3285 164.994 79.9418 164.994C79.555 164.994 79.2386 165.311 79.2386 165.697C79.2386 166.084 79.555 166.4 79.9418 166.4ZM84.2649 173.227C83.5324 173.227 82.9875 172.852 82.9875 172.207C82.9875 171.574 83.4094 171.24 84.3703 171.176L86.0695 171.064V171.645C86.0695 172.547 85.302 173.227 84.2649 173.227ZM84.0774 174.111C84.9211 174.111 85.6125 173.742 86.0227 173.068H86.1164V174H87.0774V169.676C87.0774 168.363 86.216 167.572 84.675 167.572C83.3274 167.572 82.3313 168.24 82.1965 169.254H83.216C83.3567 168.756 83.884 168.469 84.6399 168.469C85.5832 168.469 86.0695 168.896 86.0695 169.676V170.25L84.2473 170.361C82.7766 170.449 81.9445 171.1 81.9445 172.23C81.9445 173.385 82.8528 174.111 84.0774 174.111ZM94.1192 169.617C93.9434 168.492 93.0059 167.572 91.4708 167.572C89.7012 167.572 88.5762 168.85 88.5762 170.818C88.5762 172.828 89.7071 174.111 91.4766 174.111C92.9942 174.111 93.9376 173.256 94.1192 172.096H93.0997C92.9122 172.811 92.3204 173.203 91.4708 173.203C90.3458 173.203 89.6192 172.277 89.6192 170.818C89.6192 169.389 90.334 168.48 91.4708 168.48C92.379 168.48 92.9356 168.99 93.0997 169.617H94.1192ZM96.7196 170.443H96.6259V165.176H95.6181V174H96.6259V171.604L97.2294 171.041L99.5966 174H100.88L97.9794 170.391L100.698 167.684H99.4618L96.7196 170.443ZM105.219 169.412C105.219 170.326 105.758 170.836 106.942 171.123L108.026 171.387C108.7 171.551 109.028 171.844 109.028 172.277C109.028 172.857 108.419 173.262 107.569 173.262C106.76 173.262 106.256 172.922 106.087 172.389H105.049C105.161 173.438 106.128 174.111 107.534 174.111C108.969 174.111 110.065 173.332 110.065 172.201C110.065 171.293 109.491 170.777 108.301 170.49L107.329 170.256C106.585 170.074 106.233 169.805 106.233 169.371C106.233 168.809 106.819 168.428 107.569 168.428C108.331 168.428 108.823 168.762 108.958 169.266H109.954C109.819 168.229 108.899 167.572 107.575 167.572C106.233 167.572 105.219 168.363 105.219 169.412ZM114.119 168.463C115.121 168.463 115.789 169.201 115.812 170.32H112.332C112.408 169.201 113.111 168.463 114.119 168.463ZM115.783 172.365C115.519 172.922 114.968 173.221 114.154 173.221C113.082 173.221 112.384 172.43 112.332 171.182V171.135H116.873V170.748C116.873 168.785 115.835 167.572 114.13 167.572C112.396 167.572 111.283 168.861 111.283 170.848C111.283 172.846 112.378 174.111 114.13 174.111C115.513 174.111 116.486 173.449 116.79 172.365H115.783ZM118.407 174H119.414V170.086C119.414 169.195 120.112 168.551 121.073 168.551C121.272 168.551 121.635 168.586 121.717 168.609V167.602C121.588 167.584 121.377 167.572 121.213 167.572C120.375 167.572 119.649 168.006 119.461 168.621H119.367V167.684H118.407V174ZM128.085 167.684H127.007L125.278 172.887H125.185L123.456 167.684H122.378L124.716 174H125.747L128.085 167.684ZM131.67 168.463C132.672 168.463 133.34 169.201 133.363 170.32H129.883C129.959 169.201 130.662 168.463 131.67 168.463ZM133.334 172.365C133.07 172.922 132.52 173.221 131.705 173.221C130.633 173.221 129.936 172.43 129.883 171.182V171.135H134.424V170.748C134.424 168.785 133.387 167.572 131.682 167.572C129.947 167.572 128.834 168.861 128.834 170.848C128.834 172.846 129.93 174.111 131.682 174.111C133.064 174.111 134.037 173.449 134.342 172.365H133.334ZM135.958 174H136.966V170.086C136.966 169.195 137.663 168.551 138.624 168.551C138.823 168.551 139.186 168.586 139.268 168.609V167.602C139.14 167.584 138.929 167.572 138.765 167.572C137.927 167.572 137.2 168.006 137.013 168.621H136.919V167.684H135.958V174ZM144.241 166.049V167.684H143.221V168.527H144.241V172.359C144.241 173.566 144.762 174.047 146.063 174.047C146.262 174.047 146.456 174.023 146.655 173.988V173.139C146.467 173.156 146.368 173.162 146.186 173.162C145.53 173.162 145.249 172.846 145.249 172.102V168.527H146.655V167.684H145.249V166.049H144.241ZM150.632 174.111C152.431 174.111 153.544 172.869 153.544 170.842C153.544 168.809 152.431 167.572 150.632 167.572C148.833 167.572 147.72 168.809 147.72 170.842C147.72 172.869 148.833 174.111 150.632 174.111ZM150.632 173.203C149.437 173.203 148.763 172.336 148.763 170.842C148.763 169.342 149.437 168.48 150.632 168.48C151.828 168.48 152.501 169.342 152.501 170.842C152.501 172.336 151.828 173.203 150.632 173.203ZM163.644 169.617C163.468 168.492 162.53 167.572 160.995 167.572C159.226 167.572 158.101 168.85 158.101 170.818C158.101 172.828 159.232 174.111 161.001 174.111C162.519 174.111 163.462 173.256 163.644 172.096H162.624C162.437 172.811 161.845 173.203 160.995 173.203C159.87 173.203 159.144 172.277 159.144 170.818C159.144 169.389 159.858 168.48 160.995 168.48C161.903 168.48 162.46 168.99 162.624 169.617H163.644ZM167.029 173.227C166.297 173.227 165.752 172.852 165.752 172.207C165.752 171.574 166.174 171.24 167.135 171.176L168.834 171.064V171.645C168.834 172.547 168.066 173.227 167.029 173.227ZM166.842 174.111C167.685 174.111 168.377 173.742 168.787 173.068H168.881V174H169.842V169.676C169.842 168.363 168.98 167.572 167.439 167.572C166.092 167.572 165.096 168.24 164.961 169.254H165.98C166.121 168.756 166.648 168.469 167.404 168.469C168.348 168.469 168.834 168.896 168.834 169.676V170.25L167.012 170.361C165.541 170.449 164.709 171.1 164.709 172.23C164.709 173.385 165.617 174.111 166.842 174.111ZM174.733 167.572C173.872 167.572 173.116 168.012 172.712 168.738H172.618V167.684H171.657V176.109H172.665V173.051H172.759C173.104 173.719 173.831 174.111 174.733 174.111C176.339 174.111 177.387 172.816 177.387 170.842C177.387 168.855 176.345 167.572 174.733 167.572ZM174.493 173.203C173.356 173.203 172.636 172.289 172.636 170.842C172.636 169.389 173.356 168.48 174.499 168.48C175.653 168.48 176.345 169.365 176.345 170.842C176.345 172.318 175.653 173.203 174.493 173.203ZM179.343 166.049V167.684H178.324V168.527H179.343V172.359C179.343 173.566 179.865 174.047 181.166 174.047C181.365 174.047 181.558 174.023 181.757 173.988V173.139C181.57 173.156 181.47 173.162 181.289 173.162C180.632 173.162 180.351 172.846 180.351 172.102V168.527H181.757V167.684H180.351V166.049H179.343ZM188.342 167.684H187.334V171.422C187.334 172.529 186.725 173.191 185.612 173.191C184.604 173.191 184.182 172.664 184.182 171.527V167.684H183.174V171.773C183.174 173.268 183.913 174.111 185.331 174.111C186.297 174.111 186.971 173.713 187.288 173.01H187.381V174H188.342V167.684ZM190.193 174H191.201V170.086C191.201 169.195 191.898 168.551 192.859 168.551C193.058 168.551 193.421 168.586 193.503 168.609V167.602C193.374 167.584 193.163 167.572 192.999 167.572C192.161 167.572 191.435 168.006 191.247 168.621H191.154V167.684H190.193V174ZM197.059 168.463C198.061 168.463 198.729 169.201 198.752 170.32H195.272C195.348 169.201 196.051 168.463 197.059 168.463ZM198.723 172.365C198.459 172.922 197.908 173.221 197.094 173.221C196.022 173.221 195.324 172.43 195.272 171.182V171.135H199.813V170.748C199.813 168.785 198.776 167.572 197.07 167.572C195.336 167.572 194.223 168.861 194.223 170.848C194.223 172.846 195.319 174.111 197.07 174.111C198.453 174.111 199.426 173.449 199.731 172.365H198.723ZM206.631 173.227C205.898 173.227 205.353 172.852 205.353 172.207C205.353 171.574 205.775 171.24 206.736 171.176L208.435 171.064V171.645C208.435 172.547 207.668 173.227 206.631 173.227ZM206.443 174.111C207.287 174.111 207.978 173.742 208.389 173.068H208.482V174H209.443V169.676C209.443 168.363 208.582 167.572 207.041 167.572C205.693 167.572 204.697 168.24 204.562 169.254H205.582C205.723 168.756 206.25 168.469 207.006 168.469C207.949 168.469 208.435 168.896 208.435 169.676V170.25L206.613 170.361C205.142 170.449 204.31 171.1 204.31 172.23C204.31 173.385 205.219 174.111 206.443 174.111ZM216.368 167.684H215.36V171.422C215.36 172.529 214.751 173.191 213.637 173.191C212.63 173.191 212.208 172.664 212.208 171.527V167.684H211.2V171.773C211.2 173.268 211.938 174.111 213.356 174.111C214.323 174.111 214.997 173.713 215.313 173.01H215.407V174H216.368V167.684ZM220.556 174.111C221.429 174.111 222.179 173.695 222.578 172.992H222.671V174H223.632V165.176H222.625V168.68H222.537C222.179 167.988 221.435 167.572 220.556 167.572C218.951 167.572 217.902 168.861 217.902 170.842C217.902 172.828 218.939 174.111 220.556 174.111ZM220.791 168.48C221.933 168.48 222.648 169.395 222.648 170.842C222.648 172.301 221.939 173.203 220.791 173.203C219.636 173.203 218.945 172.318 218.945 170.842C218.945 169.371 219.642 168.48 220.791 168.48ZM225.565 174H226.573V167.684H225.565V174ZM226.069 166.4C226.455 166.4 226.772 166.084 226.772 165.697C226.772 165.311 226.455 164.994 226.069 164.994C225.682 164.994 225.366 165.311 225.366 165.697C225.366 166.084 225.682 166.4 226.069 166.4ZM231.042 174.111C232.841 174.111 233.954 172.869 233.954 170.842C233.954 168.809 232.841 167.572 231.042 167.572C229.243 167.572 228.13 168.809 228.13 170.842C228.13 172.869 229.243 174.111 231.042 174.111ZM231.042 173.203C229.847 173.203 229.173 172.336 229.173 170.842C229.173 169.342 229.847 168.48 231.042 168.48C232.238 168.48 232.911 169.342 232.911 170.842C232.911 172.336 232.238 173.203 231.042 173.203ZM236.203 174.059C236.625 174.059 236.965 173.713 236.965 173.297C236.965 172.875 236.625 172.535 236.203 172.535C235.787 172.535 235.442 172.875 235.442 173.297C235.442 173.713 235.787 174.059 236.203 174.059Z" fill="#272525"/>
+</svg>
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property int fontBig: 20
+ property int fontMedium: 13
+ property int fontSmall: 11
+ property int fontExtraSmall: 8
+
+ property int buttonWidth: 103
+ property int buttonHeight: 25
+
+ property int leftMargin: 48
+ property int rightMargin: 16
+
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string saveButtonBackgroundColour: "#F2F3F3"
+ property string saveButtonPressedColour: "#E7E8E8"
+ property string saveButtonStroke: "#EAEBEB"
+ property string saveButtonPressedStroke: "#B0B5B5"
+ property string saveButtonText: "#000000"
+ property string checkboxStroke: "#0062cc"
+ property string checkboxPressedStroke: "#007AFF"
+ property string disabledButtonText: "#D3D4D4"
+ property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+ property bool currShowRecommendations: virtualstudio.showWarnings
+ property string recommendationScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged")
+ property bool onWindows: Qt.platform.os === "windows"
+
+ Rectangle {
+ id: recommendationsHeader
+ x: -1
+ y: 0
+
+ width: parent.width + 2
+ height: 64
+
+ color: backgroundColour
+ border.color: "#33979797"
+
+ Image {
+ source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
+ anchors.left: parent.left
+ anchors.leftMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ width: 119 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: gettingStartedText1
+ visible: recommendationScreen === "ethernet"
+ text: "Getting Started with JackTrip (1/5)"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Text {
+ id: gettingStartedText2
+ visible: recommendationScreen === "fiber"
+ text: "Getting Started with JackTrip (2/5)"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Text {
+ id: gettingStartedText3
+ visible: recommendationScreen === "audiointerface"
+ text: "Getting Started with JackTrip (3/5)"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Text {
+ id: gettingStartedText4
+ visible: recommendationScreen === "headphones"
+ text: "Getting Started with JackTrip (4/5)"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Text {
+ id: gettingStartedText5
+ visible: recommendationScreen === "acknowledged"
+ text: "Getting Started with JackTrip (5/5)"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.right: parent.right
+ anchors.rightMargin: 32 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Item {
+ id: ethernetRecommendationItem
+ width: parent.width; height: parent.height
+ visible: recommendationScreen == "ethernet"
+
+ AppIcon {
+ id: ethernetRecommendationLogo
+ y: 90
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 179
+ height: 128
+ icon.source: "ethernet.svg"
+ }
+
+ Text {
+ id: ethernetRecommendationHeader1
+ text: "Wired Ethernet Recommended"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ethernetRecommendationLogo.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: ethernetRecommendationSubheader1
+ text: "JackTrip works best when you connect your computer directly to your Internet router via a wired ethernet cable."
+ + "<br/><br/>"
+ + "WiFi works OK for some people, but generates significantly more latency and audio glitches."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ethernetRecommendationHeader1.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ LearnMoreButton {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: ethernetRecommendationSubheader1.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ url: "https://support.jacktrip.com/wired-internet-versus-wi-fi"
+ }
+
+ Button {
+ id: okButtonEthernet
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down
+ }
+ onClicked: { recommendationScreen = "fiber" }
+ anchors.right: parent.right
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Continue"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Item {
+ id: fiberRecommendationItem
+ width: parent.width; height: parent.height
+ visible: recommendationScreen == "fiber"
+
+ AppIcon {
+ id: fiberRecommendationLogo
+ y: 90
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 179
+ height: 128
+ icon.source: "networkCheck.svg"
+ }
+
+ Text {
+ id: fiberRecommendationHeader
+ text: "Fiber Internet Recommended"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: fiberRecommendationLogo.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: fiberRecommendationSubheader
+ text: "A Fiber Internet connection from your Internet Service Provider (ISP) will give you the best experience while using JackTrip."
+ + "<br/><br/>"
+ + "It's OK to use JackTrip with Cable and DSL, but these types of Internet connections introduce significantly more latency."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: fiberRecommendationHeader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ LearnMoreButton {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: fiberRecommendationSubheader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip"
+ }
+
+ Button {
+ id: okButtonFiber
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: okButtonFiber.hovered && !okButtonFiber.down
+ }
+ onClicked: { recommendationScreen = "audiointerface" }
+ anchors.right: parent.right
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Continue"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Item {
+ id: headphoneRecommendationItem
+ width: parent.width; height: parent.height
+ visible: recommendationScreen == "headphones"
+
+ AppIcon {
+ id: headphoneWarningLogo
+ y: 90
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 118
+ height: 128
+ icon.source: "headphones.svg"
+ }
+
+ Text {
+ id: headphoneRecommendationHeader1
+ text: "Wired Headphones Required"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: headphoneWarningLogo.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: headphoneRecommendationSubheader1
+ text: "JackTrip requires the use of wired headphones."
+ + "<br/><br/>"
+ + "Using speakers will generate echos and loud feedback loops."
+ + "<br/><br/>"
+ + "Wireless and bluetooth headphones introduce higher latency."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: headphoneRecommendationHeader1.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Button {
+ id: okButtonHeadphones
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down
+ }
+ onClicked: {
+ recommendationScreen = "acknowledged";
+ }
+ anchors.right: parent.right
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Continue"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Item {
+ id: audioInterfaceRecommendationItem
+ width: parent.width; height: parent.height
+ visible: recommendationScreen == "audiointerface"
+
+ AppIcon {
+ id: audioInterfaceRecommendationLogo
+ y: 90
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: 118
+ height: 128
+ icon.source: "externalMic.svg"
+ }
+
+ Text {
+ id: audioInterfaceRecommendationHeaderNonWindows
+ visible: !onWindows
+ text: "Use Recommended Audio Devices"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: audioInterfaceRecommendationLogo.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: audioInterfaceRecommendationSubheaderNonWindows
+ visible: !onWindows
+ text: "Many audio devices are too slow to work well with JackTrip."
+ + "<br/><br/>"
+ + "We recommend external USB or Thunderbolt interfaces released within the past "
+ + "few years for the best quality, low latency and glitch-free sound."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: audioInterfaceRecommendationHeaderNonWindows.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: audioInterfaceRecommendationHeaderWindows
+ visible: onWindows
+ text: "Use Recommended Audio Devices"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: audioInterfaceRecommendationLogo.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: audioInterfaceRecommendationSubheaderWindows
+ visible: onWindows
+ text: "Many audio devices are too slow to work well with JackTrip."
+ + "<br/>"
+ + "ASIO drivers are required for low latency on Windows. "
+ + "<br/><br/>"
+ + "We recommend external USB or Thunderbolt interfaces released within the past "
+ + "few years for the best quality, low latency and glitch-free sound."
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: audioInterfaceRecommendationHeaderWindows.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ LearnMoreButton {
+ width: 250 * virtualstudio.uiScale;
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: onWindows ? audioInterfaceRecommendationSubheaderWindows.bottom : audioInterfaceRecommendationSubheaderNonWindows.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ buttonText: "See recommended devices"
+ url: "https://support.jacktrip.com/recommended-audio-interfaces"
+ }
+
+ Button {
+ id: okButtonAudioInterface
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down
+ }
+ onClicked: {
+ recommendationScreen = "headphones";
+ }
+ anchors.right: parent.right
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: 16 * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Continue"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Item {
+ id: acknowledgedRecommendationItem
+ width: parent.width; height: parent.height
+ visible: recommendationScreen == "acknowledged"
+
+ Text {
+ id: acknowledgedHeader
+ text: "Remind Me Again Next Time?"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 176 * virtualstudio.uiScale
+ }
+
+ Text {
+ id: acknowledgedSubheader
+ text: "Would you like to review the getting started recommendations again the next time you start JackTrip?"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: acknowledgedHeader.bottom
+ anchors.topMargin: 32 * virtualstudio.uiScale
+ }
+
+ Item {
+ id: acknowledgedButtonsContainer
+ width: 320 * virtualstudio.uiScale
+
+ anchors.top: acknowledgedSubheader.bottom
+ anchors.topMargin: 64 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Button {
+ id: acknowledgedYesButton
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down
+ }
+ onClicked: {
+ virtualstudio.showWarnings = true;
+ virtualstudio.saveSettings();
+ if (permissions.micPermission !== "granted") {
+ virtualstudio.windowState = "permissions";
+ } else if (virtualstudio.studioToJoin === "") {
+ virtualstudio.windowState = "browse";
+ } else {
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
+ }
+ }
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Yes"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Button {
+ id: acknowledgedNoButton
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+ layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down
+ }
+ onClicked: {
+ virtualstudio.showWarnings = false;
+ virtualstudio.saveSettings();
+ if (permissions.micPermission !== "granted") {
+ virtualstudio.windowState = "permissions";
+ } else if (virtualstudio.studioToJoin === "") {
+ virtualstudio.windowState = "browse";
+ } else {
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
+ }
+ }
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "No"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: saveButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+
+ Text {
+ id: acknowledgedSettingsInfo
+ text: "You can change this setting at any time under <b>Settings > Advanced</b>"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ width: 600
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: acknowledgedButtonsContainer.bottom
+ anchors.topMargin: 64 * virtualstudio.uiScale
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+ 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
+ }
+ onClicked: { virtualstudio.windowState = "create_studio"; }
+ anchors.right: filterButton.left
+ anchors.rightMargin: 16
+ anchors.verticalCenter: sectionText.verticalCenter
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Create Studio"
+ font.family: "Poppins"
+ font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: "#000000"
+ 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
+ }
+ 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: filterStroke
+ layer.enabled: true
+ }
+ 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;
+ 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;
+ 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
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ Rectangle {
+ width: parent.width; height: parent.height
+ color: backgroundColour
+ }
+
+ property int fontBig: 20
+ property int fontMedium: 13
+ property int fontSmall: 11
+ property int fontExtraSmall: 8
+
+ property int leftMargin: 48
+ property int rightMargin: 16
+ property int buttonWidth: 103
+ property int buttonHeight: 25
+
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC"
+ property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0"
+ property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray"
+ property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black"
+ property string warningTextColour: "#DB0A0A"
+ property string checkboxStroke: "#0062cc"
+ property string checkboxPressedStroke: "#007AFF"
+ property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+ property string errorFlagColour: "#DB0A0A"
+ property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+ property string settingsGroupView: "Audio"
+
+ function getCurrentBufferSizeIndex () {
+ let bufferSize = audio.bufferSize;
+ let idx = audio.bufferSizeComboModel.findIndex(elem => parseInt(elem) === bufferSize);
+ if (idx < 0) {
+ idx = 0;
+ }
+ return idx;
+ }
+
+ function getCurrentAudioBackendIndex () {
+ let idx = audio.audioBackendComboModel.findIndex(elem => elem === audio.audioBackend);
+ if (idx < 0) {
+ idx = 0;
+ }
+ return idx;
+ }
+
+ function getQueueBufferString () {
+ if (audio.queueBuffer == 0) {
+ return "auto";
+ }
+ return audio.queueBuffer + " ms";
+ }
+
+ Rectangle {
+ id: audioSettingsView
+ width: 0.8 * parent.width
+ height: parent.height - header.height
+ x: 0.2 * window.width
+ y: header.height
+ visible: settingsGroupView == "Audio"
+
+ AudioSettings {
+ id: audioSettings
+ }
+ }
+
+ ToolBar {
+ id: header
+ width: parent.width
+ height: 64 * virtualstudio.uiScale
+
+ background: Rectangle {
+ border.color: "#33979797"
+ color: backgroundColour
+ width: parent.width
+ }
+
+ contentItem: Item {
+ id: headerContent
+ width: header.width
+ height: header.height
+
+ Label {
+ id: pageTitle
+ text: "Settings"
+ height: headerContent.height;
+ anchors.left: headerContent.left;
+ anchors.leftMargin: 32 * virtualstudio.uiScale
+ elide: Label.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ DeviceRefreshButton {
+ id: refreshButton
+ anchors.verticalCenter: pageTitle.verticalCenter;
+ anchors.right: headerContent.right;
+ anchors.rightMargin: 16 * virtualstudio.uiScale;
+ visible: audio.audioBackend == "RtAudio" && settingsGroupView == "Audio"
+ enabled: audio.audioReady && !audio.scanningDevices
+ }
+
+ Text {
+ text: "Restarting Audio"
+ anchors.verticalCenter: pageTitle.verticalCenter;
+ anchors.right: refreshButton.left;
+ anchors.rightMargin: 16 * virtualstudio.uiScale;
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ visible: !audio.audioReady
+ }
+ }
+ }
+
+ Drawer {
+ id: drawer
+ width: 0.2 * parent.width
+ height: parent.height - header.height
+ y: header.height-1
+ modal: false
+ interactive: false
+ visible: virtualstudio.windowState == "settings"
+
+ background: Rectangle {
+ border.color: "#33979797"
+ color: backgroundColour
+ }
+
+ ButtonGroup {
+ buttons: viewControls.children
+ onClicked: function(button) {
+ settingsGroupView = button.text
+ }
+ }
+
+ Column {
+ id: viewControls
+ width: parent.width
+ spacing: 24 * virtualstudio.uiScale
+ anchors.centerIn: parent
+ Button {
+ id: audioBtn
+ text: "Audio"
+ width: parent.width
+ contentItem: Item {
+ implicitWidth: audioButtonText.implicitWidth
+ implicitHeight: audioButtonText.implicitHeight
+
+ Label {
+ id: audioButtonText
+ text: audioBtn.text
+ width: Boolean(audio.devicesError) ? parent.width - 16 * virtualstudio.uiScale : parent.width
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ color: textColour
+ }
+
+ Rectangle {
+ id: audioDevicesErrorFlag
+ anchors.left: audioButtonText.right
+ anchors.verticalCenter: audioButtonText.verticalCenter
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ width: 8 * virtualstudio.uiScale
+ height: 8 * virtualstudio.uiScale
+ color: errorFlagColour
+ radius: 4 * virtualstudio.uiScale
+ visible: Boolean(audio.devicesError)
+ }
+ }
+ background: Rectangle {
+ width: parent.width
+ color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour)
+ }
+ }
+ Button {
+ id: appearanceBtn
+ text: "Appearance"
+ width: parent.width
+ contentItem: Item {
+ implicitWidth: appearanceButtonText.implicitWidth
+ implicitHeight: appearanceButtonText.implicitHeight
+
+ Label {
+ id: appearanceButtonText
+ text: appearanceBtn.text
+ width: parent.width
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ color: textColour
+ }
+ }
+
+
+ background: Rectangle {
+ width: parent.width
+ color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour)
+ }
+ }
+ Button {
+ id: advancedBtn
+ text: "Advanced"
+ width: parent.width
+ contentItem: Item {
+ implicitWidth: advancedButtonText.implicitWidth
+ implicitHeight: advancedButtonText.implicitHeight
+
+ Label {
+ id: advancedButtonText
+ text: advancedBtn.text
+ width: parent.width
+ 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"
+ width: parent.width
+ contentItem: Item {
+
+ implicitWidth: profileButtonText.implicitWidth
+ implicitHeight: profileButtonText.implicitHeight
+
+ Label {
+ id: profileButtonText
+ text: profileBtn.text
+ width: parent.width
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ color: textColour
+ }
+ }
+ background: Rectangle {
+ width: parent.width
+ color: profileBtn.down ? buttonPressedColour : (profileBtn.hovered || settingsGroupView == "Profile" ? buttonHoverColour : backgroundColour)
+ }
+ }
+ }
+
+ Column {
+ id: appVersion
+ width: parent.width
+ spacing: 24 * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+
+ Text {
+ text: "Version " + virtualstudio.versionString
+ font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale}
+ color: textColour
+ opacity: 0.8
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ bottomPadding: 5 * virtualstudio.uiScale
+ }
+ }
+ }
+
+ Rectangle {
+ id: appearanceSettingsView
+ width: 0.8 * parent.width
+ height: parent.height - header.height
+ x: 0.2 * window.width
+ y: header.height
+ color: backgroundColour
+ visible: settingsGroupView == "Appearance"
+
+ Slider {
+ id: scaleSlider
+ x: 220 * virtualstudio.uiScale;
+ y: 100 * virtualstudio.uiScale
+ width: backendCombo.width
+ from: 1; to: 1.25; value: virtualstudio.uiScale
+ onMoved: { virtualstudio.uiScale = value }
+
+ background: Rectangle {
+ x: scaleSlider.leftPadding
+ y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
+ implicitWidth: parent.width
+ implicitHeight: 6
+ width: scaleSlider.availableWidth
+ height: implicitHeight
+ radius: 4
+ color: sliderTrackColour
+
+ Rectangle {
+ width: scaleSlider.visualPosition * parent.width
+ height: parent.height
+ color: sliderActiveTrackColour
+ radius: 4
+ }
+ }
+
+ handle: Rectangle {
+ x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width)
+ y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
+ implicitWidth: 26 * virtualstudio.uiScale
+ implicitHeight: 26 * virtualstudio.uiScale
+ radius: 13 * virtualstudio.uiScale
+ color: scaleSlider.pressed ? sliderPressedColour : sliderColour
+ border.color: buttonStroke
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: scaleSlider.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ text: "Scale Interface"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Button {
+ id: darkButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
+ x: parent.width - (232 * virtualstudio.uiScale);
+ y: scaleSlider.y + (48 * virtualstudio.uiScale)
+ width: 216 * virtualstudio.uiScale;
+ height: 30 * virtualstudio.uiScale
+ Text {
+ 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
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: darkButton.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ 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: modeButton
+ visible: virtualstudio.hasClassicMode
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: {
+ // essentially the same here as clicking the cancel button
+ audio.stopAudio();
+ virtualstudio.windowState = "browse";
+ virtualstudio.loadSettings();
+ audio.validateDevices();
+
+ // switch mode
+ virtualstudio.toClassicMode();
+ }
+ x: 220 * virtualstudio.uiScale;
+ y: 48 * virtualstudio.uiScale
+ width: 216 * virtualstudio.uiScale;
+ height: 30 * virtualstudio.uiScale
+ Text {
+ 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
+ }
+ }
+
+ Text {
+ visible: virtualstudio.hasClassicMode
+ anchors.verticalCenter: modeButton.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ text: "Display Mode"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: updateChannelCombo
+ x: 220 * virtualstudio.uiScale;
+ y: virtualstudio.hasClassicMode ? (modeButton.y + (48 * virtualstudio.uiScale)) : (48 * virtualstudio.uiScale)
+ width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+ model: virtualstudio.updateChannelComboModel
+ currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1
+ onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" }
+ font.family: "Poppins"
+ enabled: !virtualstudio.noUpdater
+ }
+
+ Text {
+ anchors.verticalCenter: updateChannelCombo.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ text: "Update Channel"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: backendCombo
+ model: audio.audioBackendComboModel
+ enabled: audio.audioBackendComboModel.length > 1
+ currentIndex: getCurrentAudioBackendIndex()
+ onActivated: {
+ audio.audioBackend = currentText
+ audio.restartAudio();
+ }
+ x: 220 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale)
+ width: updateChannelCombo.width; height: updateChannelCombo.height
+ }
+
+ Text {
+ id: backendLabel
+ anchors.verticalCenter: backendCombo.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ text: "Audio Backend"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ ComboBox {
+ id: bufferCombo
+ x: 220 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale)
+ width: backendCombo.width; height: updateChannelCombo.height
+ model: audio.bufferSizeComboModel
+ currentIndex: getCurrentBufferSizeIndex()
+ onActivated: {
+ audio.bufferSize = parseInt(currentText, 10);
+ audio.restartAudio();
+ }
+ font.family: "Poppins"
+ }
+
+ Text {
+ anchors.verticalCenter: bufferCombo.verticalCenter
+ x: 48 * virtualstudio.uiScale
+ text: "Buffer Size"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Slider {
+ id: queueBufferSlider
+ value: audio.queueBuffer
+ onMoved: {
+ audio.queueBuffer = value;
+ }
+ from: 0
+ to: 128
+ stepSize: 1
+ padding: 0
+ x: updateChannelCombo.x + queueBufferText.width
+ y: bufferCombo.y + (54 * virtualstudio.uiScale)
+ width: updateChannelCombo.width - queueBufferText.width
+
+ background: Rectangle {
+ x: queueBufferSlider.leftPadding
+ y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
+ implicitWidth: parent.width
+ implicitHeight: 6
+ width: queueBufferSlider.availableWidth
+ height: implicitHeight
+ radius: 4
+ color: sliderTrackColour
+
+ Rectangle {
+ width: queueBufferSlider.visualPosition * parent.width
+ height: parent.height
+ color: sliderActiveTrackColour
+ radius: 4
+ }
+ }
+
+ handle: Rectangle {
+ x: queueBufferSlider.leftPadding + queueBufferSlider.visualPosition * (queueBufferSlider.availableWidth - width)
+ y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
+ implicitWidth: 26 * virtualstudio.uiScale
+ implicitHeight: 26 * virtualstudio.uiScale
+ radius: 13 * virtualstudio.uiScale
+ color: queueBufferSlider.pressed ? sliderPressedColour : sliderColour
+ border.color: buttonStroke
+ }
+ }
+
+ Text {
+ id: queueBufferText
+ width: (64 * virtualstudio.uiScale)
+ x: updateChannelCombo.x;
+ anchors.verticalCenter: queueBufferSlider.verticalCenter
+ text: getQueueBufferString()
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Text {
+ id: queueBufferLabel
+ anchors.verticalCenter: queueBufferSlider.verticalCenter
+ x: leftMargin * virtualstudio.uiScale
+ text: "Adjust Latency"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ InfoTooltip {
+ id: tooltip
+ content: "JackTrip analyzes your Internet connection to find the best balance between audio latency and quality. Add additional latency to further improve quality."
+ size: 16
+ anchors.left: queueBufferLabel.right
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.verticalCenter: queueBufferSlider.verticalCenter
+ }
+
+ CheckBox {
+ id: feedbackDetection
+ checked: audio.feedbackDetectionEnabled
+ text: qsTr("Automatically mute when feedback is detected")
+ x: updateChannelCombo.x;
+ y: queueBufferSlider.y + (48 * virtualstudio.uiScale)
+ onClicked: { audio.feedbackDetectionEnabled = feedbackDetection.checkState == Qt.Checked; }
+ indicator: Rectangle {
+ implicitWidth: 16 * virtualstudio.uiScale
+ implicitHeight: 16 * virtualstudio.uiScale
+ x: feedbackDetection.leftPadding
+ y: parent.height / 2 - height / 2
+ radius: 3 * virtualstudio.uiScale
+ border.color: feedbackDetection.down || feedbackDetection.hovered ? checkboxPressedStroke : checkboxStroke
+
+ Rectangle {
+ width: 10 * virtualstudio.uiScale
+ height: 10 * virtualstudio.uiScale
+ x: 3 * virtualstudio.uiScale
+ y: 3 * virtualstudio.uiScale
+ radius: 2 * virtualstudio.uiScale
+ color: feedbackDetection.down || feedbackDetection.hovered ? checkboxPressedStroke : checkboxStroke
+ visible: feedbackDetection.checked
+ }
+ }
+ contentItem: Text {
+ text: feedbackDetection.text
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ leftPadding: feedbackDetection.indicator.width + feedbackDetection.spacing
+ color: textColour
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: feedbackDetection.verticalCenter
+ x: 48 * virtualstudio.uiScale
+ text: "Detect Feedback"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ CheckBox {
+ id: showStartupSetup
+ checked: virtualstudio.showDeviceSetup
+ text: qsTr("Show device setup screen before connecting to a studio")
+ x: updateChannelCombo.x; y: feedbackDetection.y + (48 * virtualstudio.uiScale)
+ onClicked: { virtualstudio.showDeviceSetup = showStartupSetup.checkState == Qt.Checked; }
+ indicator: Rectangle {
+ implicitWidth: 16 * virtualstudio.uiScale
+ implicitHeight: 16 * virtualstudio.uiScale
+ x: showStartupSetup.leftPadding
+ y: parent.height / 2 - height / 2
+ radius: 3 * virtualstudio.uiScale
+ border.color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
+
+ Rectangle {
+ width: 10 * virtualstudio.uiScale
+ height: 10 * virtualstudio.uiScale
+ x: 3 * virtualstudio.uiScale
+ y: 3 * virtualstudio.uiScale
+ radius: 2 * virtualstudio.uiScale
+ color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
+ visible: showStartupSetup.checked
+ }
+ }
+ contentItem: Text {
+ text: showStartupSetup.text
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ leftPadding: showStartupSetup.indicator.width + showStartupSetup.spacing
+ color: textColour
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: showStartupSetup.verticalCenter
+ x: 48 * virtualstudio.uiScale
+ text: "Device Setup"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ CheckBox {
+ id: showStartupWarnings
+ checked: virtualstudio.showWarnings
+ text: qsTr("Show recommendations on startup again next time")
+ x: updateChannelCombo.x; y: showStartupSetup.y + (48 * virtualstudio.uiScale)
+ onClicked: { virtualstudio.showWarnings = showStartupWarnings.checkState == Qt.Checked; }
+ indicator: Rectangle {
+ implicitWidth: 16 * virtualstudio.uiScale
+ implicitHeight: 16 * virtualstudio.uiScale
+ x: showStartupWarnings.leftPadding
+ y: parent.height / 2 - height / 2
+ radius: 3 * virtualstudio.uiScale
+ border.color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
+
+ Rectangle {
+ width: 10 * virtualstudio.uiScale
+ height: 10 * virtualstudio.uiScale
+ x: 3 * virtualstudio.uiScale
+ y: 3 * virtualstudio.uiScale
+ radius: 2 * virtualstudio.uiScale
+ color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
+ visible: showStartupWarnings.checked
+ }
+ }
+ contentItem: Text {
+ text: showStartupWarnings.text
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ leftPadding: showStartupWarnings.indicator.width + showStartupWarnings.spacing
+ color: textColour
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: showStartupWarnings.verticalCenter
+ x: 48 * virtualstudio.uiScale
+ text: "Recommendations"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+ }
+
+ Rectangle {
+ id: profileSettingsView
+ width: 0.8 * parent.width
+ height: parent.height - header.height
+ x: 0.2 * window.width
+ y: header.height
+ color: backgroundColour
+ visible: settingsGroupView == "Profile"
+
+ Image {
+ id: profilePicture
+ width: 96; height: 96
+ y: 60 * virtualstudio.uiScale
+ source: virtualstudio.userMetadata.picture ? virtualstudio.userMetadata.picture : ""
+ anchors.horizontalCenter: parent.horizontalCenter
+ fillMode: Image.PreserveAspectCrop
+ }
+
+ Text {
+ id: displayName
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: profilePicture.bottom
+ text: virtualstudio.userMetadata.user_metadata ? ( virtualstudio.userMetadata.user_metadata.display_name ? virtualstudio.userMetadata.user_metadata.display_name : virtualstudio.userMetadata.nickname ) : virtualstudio.userMetadata.name || ""
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Text {
+ id: email
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: displayName.bottom
+ text: virtualstudio.userMetadata.email ? virtualstudio.userMetadata.email : ""
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ Button {
+ id: editButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: editButton.down ? buttonPressedColour : (editButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: editButton.down ? buttonPressedStroke : (editButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { virtualstudio.editProfile(); }
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: email.y + (56 * virtualstudio.uiScale)
+ width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Edit Profile"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Button {
+ id: logoutButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: { virtualstudio.logout(); }
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: editButton.y + (48 * virtualstudio.uiScale)
+ width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Log Out"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Button {
+ id: testModeButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: {
+ virtualstudio.testMode = !virtualstudio.testMode;
+
+ // behave like "Cancel" and switch back to browse mode
+ audio.stopAudio();
+ virtualstudio.windowState = "browse";
+ virtualstudio.loadSettings();
+ audio.validateDevices();
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: logoutButton.y + (48 * virtualstudio.uiScale)
+ width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false
+ Text {
+ text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+ }
+
+ Rectangle {
+ x: -1; y: parent.height - (36 * virtualstudio.uiScale)
+ width: parent.width; height: (36 * virtualstudio.uiScale)
+ border.color: "#33979797"
+ color: backgroundColour
+
+ Button {
+ id: cancelButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: {
+ audio.stopAudio();
+ virtualstudio.windowState = "browse";
+ virtualstudio.loadSettings();
+ audio.validateDevices();
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ x: parent.width - (230 * virtualstudio.uiScale)
+ width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+ Text {
+ text: "Cancel"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: textColour
+ }
+ }
+
+ Button {
+ id: saveButton
+ enabled: !Boolean(audio.devicesError)
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour)
+ border.width: 1
+ border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke)
+ }
+ onClicked: {
+ audio.stopAudio();
+ virtualstudio.windowState = "browse";
+ virtualstudio.saveSettings();
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ x: parent.width - (119 * virtualstudio.uiScale)
+ width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+ Text {
+ text: "Save"
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+ color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour
+ }
+ }
+
+ DeviceWarning {
+ id: deviceWarning
+ x: (0.2 * window.width) + 16 * virtualstudio.uiScale
+ anchors.verticalCenter: parent.verticalCenter
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+ }
+
+ Connections {
+ target: audio
+ // anything that sets currentIndex to the value of a function needs
+ // to be manually updated whenever there is a change to any vars it uses
+ function onBufferSizeChanged() {
+ bufferCombo.currentIndex = getCurrentBufferSizeIndex();
+ }
+ function onAudioBackendChanged() {
+ backendCombo.currentIndex = getCurrentAudioBackendIndex();
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ property int fontBig: 20
+ property int fontMedium: 13
+ property int fontSmall: 11
+ property int fontExtraSmall: 8
+
+ property int leftMargin: 48
+ property int rightMargin: 16
+
+ property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+ property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+ property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+ property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+ property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+ property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+ property string saveButtonBackgroundColour: "#F2F3F3"
+ property string saveButtonPressedColour: "#E7E8E8"
+ property string saveButtonStroke: "#EAEBEB"
+ property string saveButtonPressedStroke: "#B0B5B5"
+ property string saveButtonText: "#000000"
+ property string checkboxStroke: "#0062cc"
+ property string checkboxPressedStroke: "#007AFF"
+ property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+ Item {
+ id: setupItem
+ width: parent.width; height: parent.height
+
+ property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+
+ Text {
+ id: pageTitle
+ x: 16 * virtualstudio.uiScale;
+ y: 16 * virtualstudio.uiScale
+ text: "Choose your audio devices"
+ font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ }
+
+ DeviceRefreshButton {
+ id: refreshButton
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.verticalCenter: pageTitle.verticalCenter
+ visible: parent.isUsingRtAudio
+ enabled: audio.audioReady && !audio.scanningDevices
+ }
+
+ Text {
+ text: "Restarting Audio"
+ anchors.verticalCenter: pageTitle.verticalCenter;
+ anchors.right: refreshButton.left;
+ anchors.rightMargin: 16 * virtualstudio.uiScale;
+ font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ visible: !audio.audioReady
+ }
+
+ AudioSettings {
+ id: audioSettings
+ width: parent.width
+ anchors.top: pageTitle.bottom
+ anchors.topMargin: 16 * virtualstudio.uiScale
+ }
+
+ Rectangle {
+ id: headerBorder
+ width: parent.width
+ height: 1
+ anchors.top: audioSettings.top
+ color: strokeColor
+ }
+
+ Rectangle {
+ id: footerBorder
+ width: parent.width
+ height: 1
+ anchors.top: audioSettings.bottom
+ color: strokeColor
+ }
+
+ Rectangle {
+ property int footerHeight: (30 + (rightMargin * 2)) * virtualstudio.uiScale;
+ x: -1; y: parent.height - footerHeight;
+ width: parent.width; height: footerHeight;
+ border.color: "#33979797"
+ color: backgroundColour
+
+ Button {
+ id: backButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: backButton.down ? buttonPressedColour : buttonColour
+ border.width: 1
+ border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke
+ }
+ onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); }
+ anchors.left: parent.left
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ anchors.bottomMargin: rightMargin * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Back"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ color: textColour
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DeviceWarning {
+ id: deviceWarning
+ anchors.left: backButton.right
+ anchors.leftMargin: 16 * virtualstudio.uiScale
+ anchors.verticalCenter: backButton.verticalCenter
+ visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+ }
+
+ Button {
+ id: saveButton
+ background: Rectangle {
+ radius: 6 * virtualstudio.uiScale
+ color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+ border.width: 1
+ border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+ }
+ enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady
+ onClicked: {
+ if (Boolean(audio.devicesWarning)) {
+ deviceWarningModal.open();
+ } else {
+ audio.stopAudio(true);
+ virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
+ virtualstudio.windowState = "connected";
+ virtualstudio.saveSettings();
+ virtualstudio.joinStudio();
+ }
+ }
+ anchors.right: parent.right
+ anchors.rightMargin: rightMargin * virtualstudio.uiScale
+ anchors.bottomMargin: rightMargin * virtualstudio.uiScale
+ anchors.bottom: parent.bottom
+ width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+ Text {
+ text: "Connect to Session"
+ font.family: "Poppins"
+ font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+ font.weight: Font.Bold
+ color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ CheckBox {
+ id: showAgainCheckbox
+ checked: virtualstudio.showDeviceSetup
+ visible: audio.backendAvailable
+ text: qsTr("Ask again next time")
+ anchors.right: saveButton.left
+ anchors.rightMargin: 16 * virtualstudio.uiScale
+ anchors.verticalCenter: saveButton.verticalCenter
+ onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked }
+ indicator: Rectangle {
+ implicitWidth: 16 * virtualstudio.uiScale
+ implicitHeight: 16 * virtualstudio.uiScale
+ x: showAgainCheckbox.leftPadding
+ y: parent.height / 2 - height / 2
+ radius: 3 * virtualstudio.uiScale
+ border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
+
+ Rectangle {
+ width: 10 * virtualstudio.uiScale
+ height: 10 * virtualstudio.uiScale
+ x: 3 * virtualstudio.uiScale
+ y: 3 * virtualstudio.uiScale
+ radius: 2 * virtualstudio.uiScale
+ color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
+ visible: showAgainCheckbox.checked
+ }
+ }
+
+ contentItem: Text {
+ text: showAgainCheckbox.text
+ font.family: "Poppins"
+ font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing
+ color: textColour
+ }
+ }
+ }
+
+ DeviceWarningModal {
+ id: deviceWarningModal
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import Qt5Compat.GraphicalEffects
+import VS 1.0
+
+Rectangle {
+ width: 664; height: 83 * virtualstudio.uiScale
+ radius: 6 * virtualstudio.uiScale
+ color: backgroundColour
+
+ property string serverLocation: "Germany - Berlin"
+ property string flagImage: "flags/DE.svg"
+ property string hostname: "app.jacktrip.com"
+ property string studioName: "Test Studio"
+ property string studioId: ""
+ property string streamId: ""
+ property string inviteKeyString: ""
+ property int sampleRate: 48000
+ property bool publicStudio: false
+ property bool admin: 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
+ property real fontSmall: 8
+
+ property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+ property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+ property string inviteToolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+ property string inviteToolTipTextColour: textColour
+ property string inviteCopiedBackgroundColour: "#57B147"
+ property string inviteCopiedTextColour: "#FAFBFB"
+ property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+ property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB"
+ property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3"
+ property string baseButtonPressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB"
+ property string baseButtonStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494"
+
+ property string joinAvailableColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
+ property string joinAvailableHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
+ property string joinAvailablePressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
+ property string joinAvailableStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
+
+ property string joinUnavailableColour: baseButtonColour
+ property string joinUnavailableHoverColour: baseButtonHoverColour
+ property string joinUnavailablePressedColour: baseButtonPressedColour
+ property string joinUnavailableStroke: baseButtonStroke
+
+ property string startColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
+ property string startHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
+ property string startPressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
+ property string startStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
+
+ property string manageColour: baseButtonColour
+ property string manageHoverColour: baseButtonHoverColour
+ property string managePressedColour: baseButtonPressedColour
+ property string manageStroke: baseButtonStroke
+
+ property string leaveColour: virtualstudio.darkMode ? "#FCB6B6" : "#FCB6B6"
+ property string leaveHoverColour: virtualstudio.darkMode ? "#D49696" : "#E3A4A4"
+ property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD"
+ property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E"
+
+ property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+ border.width: 1
+ border.color: studioStroke
+
+ Clipboard {
+ id: clipboard
+ }
+
+ Rectangle {
+ id: shadow
+ anchors.fill: parent
+ color: "transparent"
+ radius: 6
+ }
+
+ Rectangle {
+ width: 12 * virtualstudio.uiScale; height: parent.height
+ radius: width / 2
+ color: available ? "#0C1424" : "#B3B3B3"
+ }
+
+ Image {
+ id: wedge
+ source: available ? "wedge.svg" : "wedge_inactive.svg"
+ x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale
+ sourceSize: Qt.size(wedge.width,wedge.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ Image {
+ id: studioLogo
+ source: "logo.svg"
+ x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+ sourceSize: Qt.size(studioLogo.width,studioLogo.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ Rectangle {
+ x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale
+ width: 32 * virtualstudio.uiScale; height: width
+ radius: width / 2
+ color: available ? "#0C1424" : "#B3B3B3"
+ }
+
+ Image {
+ id: flag
+ source: flagImage
+ x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale
+ width: 40 * virtualstudio.uiScale; height: width / 4 * 3
+ fillMode: Image.PreserveAspectCrop
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: mask
+ }
+
+ AppIcon {
+ id: defaultFlag
+ anchors.fill: parent
+ width: 32 * virtualstudio.uiScale
+ height: 32 * virtualstudio.uiScale
+ icon.source: "language.svg"
+ color: "white"
+ visible: flag.status != Image.Ready
+ }
+ }
+
+ Rectangle {
+ id: mask
+ x: 0 ; y: 0 ; width: flag.width; height: flag.height
+ visible: false
+ color: "#00000000"
+ Rectangle {
+ x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale
+ width:24 * virtualstudio.uiScale; height: width
+ radius: width / 2
+ }
+ }
+
+ Text {
+ x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale;
+ width: (admin || connected) ? 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
+ }
+
+ Rectangle {
+ id: publicRect
+ x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale
+ width: 14 * virtualstudio.uiScale; height: width
+ radius: 2 * virtualstudio.uiScale
+ color: publicStudio ? "#0095FF" : "#FF9800"
+ Image {
+ id: pubPriv
+ source: publicStudio ? "public.svg" : "private.svg"
+ x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width
+ sourceSize: Qt.size(pubPriv.width,pubPriv.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ Text {
+ anchors.verticalCenter: publicRect.verticalCenter
+ x: (leftMargin + 22) * virtualstudio.uiScale
+ width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale)
+ text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+ elide: Text.ElideRight
+ color: textColour
+ }
+
+ Button {
+ id: joinButton
+ x: (admin || connected) ? 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
+ color: available ? (joinButton.down ? joinAvailablePressedColour : (joinButton.hovered ? joinAvailableHoverColour : joinAvailableColour))
+ : (joinButton.down ? joinUnavailablePressedColour : (joinButton.hovered ? joinUnavailableHoverColour : joinUnavailableColour))
+ border.width: joinButton.down ? 1 : 0
+ border.color: available ? joinAvailableStroke : joinUnavailableStroke
+ }
+ visible: !connected
+ onClicked: {
+ virtualstudio.studioToJoin = studioId;
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
+ }
+ Image {
+ id: join
+ width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ source: "join.svg"
+ sourceSize: Qt.size(join.width,join.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ Button {
+ id: leaveButton
+ x: (admin || connected) ? 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
+ color: leaveButton.down ? leavePressedColour : (leaveButton.hovered ? leaveHoverColour : leaveColour)
+ border.width: leaveButton.down ? 1 : 0
+ border.color: leaveStroke
+ }
+ visible: connected
+ onClicked: {
+ virtualstudio.disconnect();
+ }
+ Image {
+ id: leave
+ width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ source: "leave.svg"
+ sourceSize: Qt.size(leave.width,leave.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ Text {
+ anchors.horizontalCenter: joinButton.horizontalCenter
+ y: 56 * virtualstudio.uiScale
+ text: connected ? "Leave" : "Join"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+ visible: true
+ color: textColour
+ }
+
+ Button {
+ id: inviteButton
+ x: (admin || connected) ? 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: 3000; running: false; repeat: false
+ onTriggered: inviteCopied = false;
+ }
+ onClicked: {
+ inviteCopied = true;
+ if (virtualstudio.testMode) {
+ hostname = "test.jacktrip.com";
+ }
+ if (!inviteKeyString) {
+ clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=true"));
+ } else {
+ clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=" + inviteKeyString));
+ }
+ copiedResetTimer.restart()
+ }
+ visible: true
+ Image {
+ id: shareImg
+ width: 24 * virtualstudio.uiScale; height: width
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ source: "share.svg"
+ sourceSize: Qt.size(shareImg.width,shareImg.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ ToolTip {
+ parent: inviteButton
+ visible: !inviteCopied && inviteButton.hovered
+ bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
+ rightPadding: rightToolTipMargin * virtualstudio.uiScale
+ delay: 100
+ contentItem: Rectangle {
+ color: inviteToolTipBackgroundColour
+ radius: 3
+ anchors.fill: parent
+ anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
+ anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
+ layer.enabled: true
+ border.width: 1
+ border.color: tooltipStroke
+
+ Text {
+ anchors.centerIn: parent
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
+ text: qsTr("Copy invite link for Studio")
+ color: inviteToolTipTextColour
+ }
+ }
+ background: Rectangle {
+ color: "transparent"
+ }
+ }
+ ToolTip {
+ parent: inviteButton
+ visible: inviteCopied
+ bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
+ rightPadding: rightToolTipMargin * virtualstudio.uiScale
+ delay: 100
+ contentItem: Rectangle {
+ color: inviteCopiedBackgroundColour
+ radius: 3
+ anchors.fill: parent
+ anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
+ anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
+ layer.enabled: true
+ border.width: 1
+ border.color: tooltipStroke
+
+ Text {
+ anchors.centerIn: parent
+ font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
+ text: qsTr("📋 Copied invitation link to Clipboard")
+ color: inviteCopiedTextColour
+ }
+ }
+ 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
+ x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale
+ width: 40 * virtualstudio.uiScale; height: width
+ background: Rectangle {
+ radius: width / 2
+ color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour)
+ border.width: manageButton.down ? 1 : 0
+ border.color: manageStroke
+ }
+ onClicked: {
+ var url = "";
+ if (streamId === "") {
+ if (virtualstudio.testMode) {
+ url = "https://test.jacktrip.com/studios/" + studioId;
+ } else {
+ url = "https://app.jacktrip.com/studios/" + studioId;
+ }
+ } else {
+ if (virtualstudio.testMode) {
+ url = "https://next-test.jacktrip.com/@" + streamId + "/dashboard";
+ } else {
+ url = "https://www.jacktrip.com/@" + streamId + "/dashboard";
+ }
+ }
+ virtualstudio.openLink(qsTr(url));
+ }
+ visible: admin || connected
+ Image {
+ id: manageImg
+ width: 20 * virtualstudio.uiScale; height: width
+ anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+ source: "manage.svg"
+ sourceSize: Qt.size(manageImg.width,manageImg.height)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ Text {
+ anchors.horizontalCenter: manageButton.horizontalCenter
+ y: 56 * virtualstudio.uiScale
+ text: "Manage"
+ font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+ visible: admin || connected
+ color: textColour
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+ width: parent.width
+ height: parent.height
+
+ required property string labelText
+ required property string tooltipText
+ required property bool sliderEnabled
+ property bool showLabel: true
+ property int fontSize: 8
+
+ property string iconColor: virtualstudio.darkMode ? "#ACAFAF" : "gray"
+ property string sliderPressedColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC"
+
+ Item {
+ anchors.fill: parent
+ anchors.verticalCenter: parent.verticalCenter
+
+ Text {
+ id: label
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: 40 * virtualstudio.uiScale
+ horizontalAlignment: Text.AlignRight
+ text: labelText
+ font {family: "Poppins"; weight: Font.Medium; pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale }
+ color: textColour
+ visible: showLabel
+ }
+
+ InfoTooltip {
+ id: tooltip
+ content: tooltipText
+ size: 16
+ anchors.left: label.right
+ anchors.leftMargin: 2 * virtualstudio.uiScale
+ anchors.verticalCenter: label.verticalCenter
+ visible: showLabel
+ }
+
+ AppIcon {
+ id: quieterIcon
+ anchors.left: showLabel ? tooltip.right : parent.left
+ anchors.leftMargin: showLabel ? 8 * virtualstudio.uiScale : 0
+ anchors.verticalCenter: label.verticalCenter
+ width: 16
+ height: 16
+ icon.source: "quiet.svg"
+ color: iconColor
+ }
+
+ AppIcon {
+ id: louderIcon
+ anchors.right: parent.right
+ anchors.verticalCenter: label.verticalCenter
+ width: 18
+ height: 18
+ icon.source: "loud.svg"
+ color: iconColor
+ }
+
+ Slider {
+ id: slider
+ value: labelText == "Monitor" ? audio.monitorVolume : (labelText == "Studio" ? audio.outputVolume : audio.inputVolume )
+ onMoved: {
+ if (labelText == "Monitor") {
+ audio.monitorVolume = value;
+ } else if (labelText == "Studio") {
+ audio.outputVolume = value;
+ } else {
+ audio.inputVolume = value;
+ }
+ }
+ enabled: sliderEnabled
+ from: 0.0
+ to: 1.0
+ stepSize: 0.01
+ padding: 0
+ anchors.left: quieterIcon.right
+ anchors.leftMargin: 4 * virtualstudio.uiScale
+ anchors.right: louderIcon.left
+ anchors.rightMargin: 4 * virtualstudio.uiScale
+ anchors.verticalCenter: label.verticalCenter
+
+ background: Rectangle {
+ x: slider.leftPadding
+ y: slider.topPadding + slider.availableHeight / 2 - height / 2
+ implicitWidth: parent.width
+ implicitHeight: 8 * virtualstudio.uiScale
+ width: slider.availableWidth
+ height: implicitHeight
+ radius: 4
+ color: "gray"
+
+ Rectangle {
+ width: slider.visualPosition * parent.width
+ height: parent.height
+ radius: 4
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+ GradientStop { position: 0.0; color: sliderEnabled ? "#67C6F3" : "light gray" }
+ GradientStop { position: 1.0; color: sliderEnabled ? "#00897b" : "light gray" }
+ }
+ }
+ }
+
+ handle: Rectangle {
+ x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
+ y: slider.topPadding + slider.availableHeight / 2 - height / 2
+ implicitWidth: 18 * virtualstudio.uiScale
+ implicitHeight: 18 * virtualstudio.uiScale
+ radius: implicitWidth / 2
+ color: slider.pressed ? sliderPressedColour : "white"
+ border.width: 3
+ border.color: sliderEnabled ? "#00897b" : "light gray"
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Loader {
+ anchors.fill: parent
+ source: "WebEngine.qml"
+
+ // TODO: Add support for QtWebView
+ // source: useWebEngine ? "WebEngine.qml" : "WebView.qml"
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtWebEngine
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ function contentScriptFactory (port) {
+ return `
+ // add script tag for qwebchannel
+ document.head.addEventListener("initqwebchannel", () => {
+
+ var script = document.createElement("script");
+ script.onload = function () {
+ var url = "ws://localhost:${port}";
+ var socket = new WebSocket(url);
+
+ socket.onclose = function() {
+ console.error("[QT] web channel closed");
+ };
+ socket.onerror = function(event) {
+ console.error("[QT] web channel error: " + event.type);
+ };
+ socket.onopen = function() {
+ new QWebChannel(socket, function(channel) {
+ console.log("[QT] Socket opened");
+
+ // make core object accessible globally
+ window.virtualstudio = channel.objects.virtualstudio;
+ window.auth = channel.objects.auth;
+ window.clipboard = channel.objects.clipboard;
+
+ const event = new CustomEvent("qwebchannelinitialized");
+ document.head.dispatchEvent(event);
+ console.log("[QT] Dispatched qwebchannelinitialized event");
+ console.log("[QT] Connected to WebChannel, ready to send/receive messages!");
+ });
+ }
+ }
+ script.setAttribute("src", "qrc:///qtwebchannel/qwebchannel.js");
+ script.setAttribute("type", "text/javascript");
+ document.head.appendChild(script);
+ console.log("[QT] Added qwebchannel initialization script to DOM.");
+ });
+ console.log("[QT] Added initqwebchannel event listener");
+ `
+ }
+
+ Rectangle {
+ id: web
+ anchors.fill: parent
+ color: backgroundColour
+
+ property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+ property string studioId: virtualstudio.currentStudio.id
+
+ WebEngineView {
+ id: webEngineView
+ anchors.fill: parent
+ settings.javascriptCanAccessClipboard: true
+ settings.javascriptCanPaste: true
+ settings.screenCaptureEnabled: true
+ profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
+ url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+
+ // useful for debugging
+ // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) {
+ // console.log(level, message, lineNumber, sourceID);
+ // }
+
+ // useful for debugging
+ // onLoadingChanged: function(loadRequest) {
+ // console.log("onLoadingChanged", loadRequest.errorCode, loadRequest.errorDomain, loadRequest.errorString, loadRequest.status, loadRequest.url);
+ // }
+
+ 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);
+ }
+
+ onNavigationRequested: function(request) {
+ webEngineView.userScripts.collection = [
+ {
+ name: "script",
+ sourceCode: contentScriptFactory(virtualstudio.webChannelPort),
+ injectionPoint: WebEngineScript.DocumentReady,
+ worldId: WebEngineScript.MainWorld
+ }
+ ]
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ Item {
+ id: webNull
+ anchors.fill: parent
+ }
+}
--- /dev/null
+/****************************************************************************
+**
+** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
+*author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "WebSocketTransport.h"
+
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QtWebSockets/QWebSocket>
+
+/*!
+ \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally.
+ The transport delegates all messages received over the QWebSocket over its
+ textMessageReceived signal. Analogously, all calls to sendTextMessage will
+ be send over the QWebSocket to the remote client.
+*/
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ Construct the transport object and wrap the given socket.
+ The socket is also set as the parent of the transport object.
+*/
+WebSocketTransport::WebSocketTransport(QWebSocket* socket)
+ : QWebChannelAbstractTransport(socket), m_socket(socket)
+{
+ connect(socket, &QWebSocket::textMessageReceived, this,
+ &WebSocketTransport::textMessageReceived);
+}
+
+/*!
+ Destroys the WebSocketTransport.
+*/
+WebSocketTransport::~WebSocketTransport() {}
+
+/*!
+ Serialize the JSON message and send it as a text message via the WebSocket to the
+ client.
+*/
+void WebSocketTransport::sendMessage(const QJsonObject& message)
+{
+ QJsonDocument doc(message);
+ m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
+}
+
+/*!
+ Deserialize the stringified JSON messageData and emit messageReceived.
+*/
+void WebSocketTransport::textMessageReceived(const QString& messageData)
+{
+ QJsonParseError error;
+ QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error);
+ if (error.error) {
+ qWarning() << "Failed to parse text message as JSON object:" << messageData
+ << "Error is:" << error.errorString();
+ return;
+ } else if (!message.isObject()) {
+ qWarning() << "Received JSON message that is not an object: " << messageData;
+ return;
+ }
+ emit messageReceived(message.object(), this);
+}
+
+QT_END_NAMESPACE
\ No newline at end of file
--- /dev/null
+/****************************************************************************
+**
+** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
+*author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef WEBSOCKETTRANSPORT_H
+#define WEBSOCKETTRANSPORT_H
+
+#include <QtWebChannel/QWebChannelAbstractTransport>
+
+QT_BEGIN_NAMESPACE
+
+class QWebSocket;
+class WebSocketTransport : public QWebChannelAbstractTransport
+{
+ Q_OBJECT
+ public:
+ explicit WebSocketTransport(QWebSocket* socket);
+ virtual ~WebSocketTransport();
+
+ void sendMessage(const QJsonObject& message) Q_DECL_OVERRIDE;
+
+ private Q_SLOTS:
+ void textMessageReceived(const QString& message);
+
+ private:
+ QWebSocket* m_socket;
+};
+
+QT_END_NAMESPACE
+
+#endif // WEBSOCKETTRANSPORT_H
\ No newline at end of file
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+import QtWebView
+
+Item {
+ width: parent.width; height: parent.height
+ clip: true
+
+ Item {
+ id: web
+ anchors.fill: parent
+
+ property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+ property string studioId: virtualstudio.currentStudio.id
+
+ WebView {
+ id: webEngineView
+ anchors.fill: parent
+ httpUserAgent: `JackTrip/${virtualstudio.versionString}`
+ url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+ }
+ }
+}
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="240" viewBox="0 96 960 960" width="240"><path d="M421 676.692 320.077 574q-7.154-5.385-16.615-5.769-9.462-.385-15.847 6-7.154 7.154-7.154 16.615 0 9.462 7.154 15.616l109.923 110.154q9.049 11 23.371 11t24.553-11l227.153-226.385q5.616-6.385 6-15.846.385-9.462-6-16.847-7.384-6.153-16.961-6.038-9.577.115-15.731 6.269L421 676.692ZM480.134 952q-78.082 0-146.274-29.859-68.193-29.86-119.141-80.762-50.947-50.902-80.833-119.033Q104 654.215 104 576.134q0-77.569 29.918-146.371 29.919-68.803 80.922-119.917 51.003-51.114 119.032-80.48Q401.901 200 479.866 200q77.559 0 146.353 29.339 68.794 29.34 119.922 80.422 51.127 51.082 80.493 119.841Q856 498.361 856 575.95q0 78.358-29.339 146.21-29.34 67.853-80.408 118.902-51.069 51.048-119.81 80.993Q557.702 952 480.134 952ZM480 908.231q137.897 0 235.064-97.282Q812.231 713.666 812.231 576q0-137.897-97.167-235.064T480 243.769q-137.666 0-234.949 97.167Q147.769 438.103 147.769 576q0 137.666 97.282 234.949Q342.334 908.231 480 908.231ZM480 576Z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 13.5C9.07177 13.5 8.18153 13.1313 7.52515 12.4749C6.86877 11.8185 6.50002 10.9283 6.50002 10C6.50002 9.07174 6.86877 8.1815 7.52515 7.52513C8.18153 6.86875 9.07177 6.5 10 6.5C10.9283 6.5 11.8185 6.86875 12.4749 7.52513C13.1313 8.1815 13.5 9.07174 13.5 10C13.5 10.9283 13.1313 11.8185 12.4749 12.4749C11.8185 13.1313 10.9283 13.5 10 13.5V13.5ZM17.43 10.97C17.47 10.65 17.5 10.33 17.5 10C17.5 9.67 17.47 9.34 17.43 9L19.54 7.37C19.73 7.22 19.78 6.95 19.66 6.73L17.66 3.27C17.54 3.05 17.27 2.96 17.05 3.05L14.56 4.05C14.04 3.66 13.5 3.32 12.87 3.07L12.5 0.42C12.46 0.18 12.25 0 12 0H8.00002C7.75002 0 7.54002 0.18 7.50002 0.42L7.13002 3.07C6.50002 3.32 5.96002 3.66 5.44002 4.05L2.95002 3.05C2.73002 2.96 2.46002 3.05 2.34002 3.27L0.340022 6.73C0.210022 6.95 0.270023 7.22 0.460023 7.37L2.57002 9C2.53002 9.34 2.50002 9.67 2.50002 10C2.50002 10.33 2.53002 10.65 2.57002 10.97L0.460023 12.63C0.270023 12.78 0.210022 13.05 0.340022 13.27L2.34002 16.73C2.46002 16.95 2.73002 17.03 2.95002 16.95L5.44002 15.94C5.96002 16.34 6.50002 16.68 7.13002 16.93L7.50002 19.58C7.54002 19.82 7.75002 20 8.00002 20H12C12.25 20 12.46 19.82 12.5 19.58L12.87 16.93C13.5 16.67 14.04 16.34 14.56 15.94L17.05 16.95C17.27 17.03 17.54 16.95 17.66 16.73L19.66 13.27C19.78 13.05 19.73 12.78 19.54 12.63L17.43 10.97Z" fill="#494646"/>
+</svg>
--- /dev/null
+<svg width="179" height="128" viewBox="0 0 179 128" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <image id="b" x="0" y="0" width="179" height="128" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALMAAACACAYAAABA+ljwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABLPSURBVHgB7V0HsCxFFT0GjIiICeNrMGMAMcf/AMtQRkRFUeSpKIgKmFBA2fkqiGJE+CgGnmJAxZzje5SlRZkFFDHNA8UESFCy8O3j7NTv7Z2e7p7pnrA7p+rU/2+mZ6Z79uzd2923bwMDBvhjNOaAAb3GwZIbxzwcAwb0FK/HJiF3TtDXQXs4QHJXyftLXh/zh39L/kjydZK/R/dBIb/VcO4IyUMxp/g4pr/h88y90W3wC1dW/wslb4U5BH+W+iCwJnmt5H3RTYxgF/KDMac4B/0QWNM8Bt2DzSJfLPkg7ZpFzAkE+iGsNvhHdAsHoby+F0nuoF1z2PjcXIxyCPRDWG0wRXcwQnldz5Pc0XLNrmgYszyK8ATJq1Afu0gegvnBayWTkvO0yDtJnqEcG2nXnCj5Rcw4BJqxcuchHB4JNFLnFO1jhPI6/kPy3pZrPiF5XcwBBJoRxgrCQQCN1DlFu3gNyuvHUQvdRx5pZT4teT2tDMU9kz60QDPC+BzC4UaSlwPR65yiPbwadiHfU7tmpJU5GZNu62bI3I38/MwJWqAZMR+FsEiB6HVuS8yHobxe58Jukekfb6aVOVYrk2DGINCMmEPPqP0AiF7nFM3jVbBbZF3IiVbma5I3VM7zl+yTaEHIfR/N+K7k9wqOfx9h8XZkH5oOxpdsjX7ijZJvKjn/F8mdMRk3kmAy9PPbkrtJXqkce7/kc5S/12NSzAuST5N8L3oOgbDW7GC0i1X00zK/0lKXSzBtkddrZWgwbqyV+QrKLfJtJH8zPtd7H1ogrJj3QLv4GvonZpuQc46Ua7ZHFj+SnztF8ibK+c2RdQDV69+sPZe/YGcq56+QvBd6DIGwYt4F7WIZ/RLzG+BXp5Fy7eOQCfqHkjdVjjOM+GMot8i3kzxLK7OEnkMgrJgX0S6W0R8xH4hq9Rop93iq5M2UvynkE7XyuvtwB8nfaWV218oI9BACYcV8f7SLo9APMR+KenUbFdyTs3y6kBOtjEDWgVTLvFAr8xhkoyZFz/BC10YzvoFsytQVMQXgghX4BaUvonkrxBGXt6Aeni/5bmQdwxxflnyi8rc+anEnZKNNd9Huc6Ly9yMkv45sjJrXXobwcwTRIGD+9m/A7EPAPAET44u5P+r/YvwRxV/Ap2NTpzDRzt1pfF1+D5bTLfIzJK/GZPsFegQB80ubF3AEpgkxH4Iwrs+dlXvyl/xZyt/sFCbac1n+T9p9XqKVoUW/CD0WMiEwiPmZiC/mVyCMRVaFzACiVZh9aGIB00LeRyvD8NFrUC5k3v9YdBwCg5hjizmUkIVyTwr5JK2MLugFTLtQ+2llOPN3MexCzs93WtACg5hjivn1iGORTZNDqqD3RJY+IT+3v1a3xyLrQJYJWZ9l5Cr+NtNhlEJgEHMsMb8c4S3yFrDPciZK+bxTeKBWt70wOYNYJOSRdt8T0HEIDGKOIWbb4lMX8vm6Rf6047Uj5bptMd3eS+EnZAb0qyGltM67oWMQGMQcWswvQxiLbOrsuXJUULenFLRRaGV0IX9eO69Ol78LHYLAIOYiMXMG7GXwRyghC+WeFPJXK95rpNxnAf6uxbJ2nkI+QSuzMzoCAfOLEJgPcPWzbq12hD9sa/ZcyGfrFvmkmvccKfd7ATJBuwiZLo0aiUchf1grUxZ/3TgEyl/EPphtfAGT7f2m5O3hj/1QX8hFnb2qFlnneuW+u8Au5I9ictSiyCLrIaVbomUIlL+ETvlEEZBiU1uPKCm3Rck521KnqhbZtbNXxUKrOFIrRx95c+V8kZAT7R75LKMpK2kjEBjEzHa+tKQMg2+4iPTAgnMvRRiLXLezV1XQI+08A5F0i6zHRushpaz7acr5d6MlCAxiPqHkvB6qqeY83hFhhCyUe4Z0LVwEzWyh+cQJF72qyWKKLLJuebfC9HQ52UqkncB8i3kFWS9fB/0//twWvZMXKeVs+S3KmGLaIn+qxv2qCpq/PHRpVFeqSMhHYhJFAUymZzQCgfkW86MLjgnJP6D8vai57qoIWnctKKTQPrKPoFVQyB/RyurpfXXXoojLiAj2aLfRjgmEEzPHG/ceczvl+I7K8Z2U49soxx+jHN9SOb6n9oy9Faq9592U49ugHmwfUk411sFH0CnCD79V5QFa24ss8nqtjM0ik8chUgwHpyGXxw+JKeZl5Tq1s5Qox09Qjj9VOb5iqNPF2jPUuqnpqX6qHH8+quNg+InBV9DsSDbV2bPxdEwP0a2T/I9S5p3aeRchfygvHDpbI60Xk7LshQE2CPjnjmDilNzl4Af/Gkt55qlQF6Byenkdmsea5JPH/xL5rxxTFlArFOV7kH1Bc7Czt4ryXz4K+cWIAA7+/wKT3xofy8xl8D4D4cvot2U+A9WtnI+FVttK43VKjedWYSp5a6UOd5T8FSZ9aH0GtFXXgiL8ZcEDfcTsi2X0V8z6lHZsQasLTx8e4Nmu5BdWKM/mr4T67kaYRqudPf6UnWV46CDmaQhMhkQ2IegUky7luwI9f6PlmUJ5JvNnFBm8kVKmlkWu6zPfAFm8wd0xwBWfwWRATR3Qh84FTR/aFHknkKWuzcGAnfMRD79GNpK0Nv6bv9wM8t++oGyCTNAU8irKfWQGHuWzoFOoK+bjkf1sDXADf0EehLBQO4VM1/BaQ7mtlP9zZXSCOFiTfBImhfxjFAs5R4Ksv2UTcmlnr46Y2RPfCwNcITA9hhoK/CxyC/0OTAuaEySqK7YA+0hIFawhm7JeG//NHHOrkndzuHarknMfQCbkUne0qpgPwXztwBQCnKjYAvGguhyqoJlm9tnI4ooJCnkV4ePHc9ci3xyJFpkZqrZHPdAi7wuHflWV9FyceZuLTQsDgmv0HoL4yBN4H41M0H9GFkiUI5aQ1zDtWqwijJCdx5F9LbNAtvzbBexgXIQBAs0mRaeg81RYdC8uHf8/pkVWXYs8aCqUkK0WOYePZabwv4PMD3IB17pdiAFMwt30qgj9M4ptkVXXgvmbt0M9eAuZ8BEz98C4q2NZJiNZxQD6rQ9As+BMquoGxhQyLXIuZGZDpbGrK2R29ozDb2VwdTPo7yWOZb8k+Tb4Q2C2IND8sp4QQub2DFzSdUFJmaLOHjfr2QH14NzZK4KLmDlkchLcsIYs11kVrGC2BM2ZquuhOXDYr66Q2cd5GLIVLg9EttBUxxomO3scoWG8R93E716dvargGivXKcynW+4lLNezs/IPR15quM50/ELtWeq9XI6frxy/YnxsDcVi2Qdxp4p1Hqo9fwH+G3Hy/awraAtdiXyKmfdUg4Zc0ne5kEKOEjSk4l4eFTre4X4C9RrdNe5uaOPFDdYhlJBtLsISptcP/qRCfXUuo6HkiGc5Vogvz2Vzxy2B2o3vCt9naKNAM9sTFwm5yrP/hmkh0/d+JczIp6jr1p8bYDYi5CW4V2rJ/bb4LFD7JbTNFOXJWxYQX9AHBXimbTthuhd6rDEt8qk16p3zQ2gI3P/Y9cWk8MPWaM5yxeIDYYeI2M5Ee9YCqgn5ntp9EkPZj4zbw3jkUEJuxCIT+8K9Ykvwh0DWU97YQybwa2ca+PmxOnuLlmvOwaatguswmmthuukaivM76OASdteJlCJwV6K7oF/4keRV2rERMoEcXVBeINywY6hxZI4Rn1pwLkHc/BPMVrSETNSNYAlxrfKsQUj+F9n7OLCkTIqwFrnKfYs6exyFUlesJDXr2bhFLoPryzkXAwh9K13TKIBAdUGX5VNzvW9RZ2+Dcj6moBvr7Kl4CtwrOA+bUNrApUhF7+YwQ3mBaoLmuHXZChXbfYs6exsKyqkx6kmFepqE3LhFJnxSNtWdh+87BCYTmOjct+S6FP6ioCCrCJrXPVQre2zJc0Ja6FZcC0LAvZKnY8ALYH9PpqVJAtUtdFmWfd5XnYa/HNNC3uDwnBCCXkaLWIJ7Rd+JAQSD4G3v6iDDtQJxXQ6bjxxT0I3EWpRhFe6VHVZkb4KLoF9nuFYgnoUuGrXwfY4q6EMdr2mls6dia7g3kGOsrX7rOgjmULa9t5HhWoE4FlqFj0XW6dMpbN0iE3vAvXErmF/QTxaGc7RitndnWgsoJP8F988gJ6+xCfrYCvcts9CJoQxXiHTCyC3DvWHvwXxCIBNPCrOgmX/Yx9LlYLD7tXD/DHQLbXI5jql4T19Bfxgdgs+ce+e2fW0IX8emd5DCLGgXl0OdzXsOslGHOkIrcjlCCrlM0AxC6ozbuQC/Bi1i/lC0y1OKcgt9DcrfI12O50lejTBC46/GTuPnV+nsufKFSjt3Rcf6T9wSwacxt8R8QUiejeJ3kcIs6CXEE5SJtNAnN/CczmazSuDXkHkDP7iy95Ging/dV+6PDsInrvg8zB+YfnYZ1QXtMsoxCDoQGJ/rWvkU8wsmILS9G1NKVpdOYV/ZKZeDKwgGMbvBFohVJugmfNk2yFwbrW/KniMPLB/EvAmmiEC6HDYLzc7ittp1S+iPOH35cnQIPhU/G7MPgaydZXsS2iw0r8+XgzE38lXohzB9uR4dg0/lL8XsQ+0QH2co49IppPvmEoRUhVWmvkPzTDSbfswJvo3YDLML7hylt/e4kvI2lyMGEzSbaKaI56CjeQF9G3IPzCa4UtwkkLI1eE1uqJ4ozxVoR9B85iPQUfg2xpYcsa+wxTKUuRxfRbNCziHQrKCZlb8zIxdF8G3QqzGb4F6GNmGUdQpjC/oNhucKxBc0778zegDfhn0MswuBLIVCWftNFvrJiCuoMkEza30a6ZlcIhdzl6ygYGIQn8b9BbMNAbswdEEz+q1qPLIvRyX1Pj3gc1IU52vuNH4P/4beEbMBUxpeuhw2C527HFz93GQ+ZjK2y8EFGJ32jU2g2+Db2E7N+lSEQPZF3q3kvE3QDNiPYZG59ZgtHjqGoPk+Hoke4yj4N3oF/Uf+JWZwvEnQLp3C0EzGz+bMYZOC5i/NzdFz7An/F34JevozNIaewIWC3t1QVsBuoUMLOQeXVMUWNP3sRcwImJK2youPsZF4ExAwRwqWuRwuwqhD08SMi4UeGa4VKO8U0je+CWYI3D7tn/B/+T9BP3EizG1iQNBzDdfR5fAd+fFhKnk3w7NDW2j+vRNmFFUH/BfRP5gyd+Y0+dDc5+4ixBMzyWFPUVztYILmBvE3wwyDSbKrvPwV9BP80G2CVqftmZfiAsQVck7656b4l7qdwnWYAzDIpurLX0Q/YbPQdDko6PuhOSHnTDGdTzmHi4U2ZU6aG5yCai9+Ff2FzUKTMSZEEthXxf8d2aaiRWAGpKqdwrkAG1/1w9kD/UWddlcVco7EUpa5lu9tqDffeZmgy+KwZx4cN7Z9203kyoc+J4cZoXkh50gs1zC9g0AxigS9hhkeqfDBt1H9gzoZ/YaLy1GHv4V5oimBXdD3NVyrdgqPQb8ns4JiEfU+sL3RbzAJYExB/xLmSYq3Wa5dQ9YZLQLDT3sRb9w0fJLC6GRnaXv0Exx+44aPGyNzFdUtNEdVtsUAZ/hsn1bEFB1d7FiC+yDbA2RjQ6SFNgW8J5ZrXRKMD1BA/66uoLdGP/AAxFm6b7vnzyRvYahTYrm2c/kquownov6HyQTmAt3GXoiToCVBtpTJtvLj5zCPAiUF5TndPYxUVMB3Uf9DTdFdQbvunlRFyDlcBH2a5FaGOiZKufdhGKmoDIEwHaJU8iHoDuircufQ2ELO4Spok1AZN7MOA2oj5NjrCO3j0Ygbl3y84bkU9BmWa38teRsMiIpTEe7D5gcq0Dxo9d6OeCJW+cGSOpxmuZb9jL50nHsJgfDDVkdI3h7xcSNk2/3+HQhafxs/YKjPrSV/UXIdF8cegAFRwWD10B/4GrIhpjsjPGgFKYqYK0NsNC2DoqCLfOgU87mTVys4EnE+dCY7/7jk41Efi8iixZqYADka9jjnowz11AXNdztTa/H6gNgZLzkd/gVkrsEisoTdRfvMXR+Z+/MoZL19Bjk1OYN3+LgeDP4521L2SBSDgv7SuA0DWgCHtUJ2CF14JTKXJFVYd0fTOnyr9k4oaJuFPhwDOgkK+ldoT0xt0jSN7GKhy3I8D2gRnIJlqoE+CDAUE5TDxUIPU9EdBUcM6gTz94UMfH8J3MBt04oE/Tt0ONv8gE2gP9gXYfqSKzweCj+oLge/CBzR6E1+4wHAG9EvkbpwVfK2qAYK+qcYYip6C2b8SdEvwRaRs3AjDJh7MJnMBvRLvCoH33bAFB6HfllpjlsnKJ6gGTDg/+DOVD4bzTdNuhSfQf/WLA5oCQJZHEPT+37Y+C3Jh2HAgAq4HbJIthTtCZgZ/zkTtwMGDAgEJixZRvx8xySj8lYk98Owhm5AZDwWWVoprlQOJWAu8+cGPEsYBDw36FrvncJ7MLKkLAz/3G58LOfmyOp8GTKrzhm2v0r+SfJMZNF1TM17LgbMHf4H4Vg2S7fqhVUAAAAASUVORK5CYII="/>
+</svg>
--- /dev/null
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 8.29504L6 14.295L7.41 15.705L12 11.125L16.59 15.705L18 14.295L12 8.29504Z" fill="black"/>
+</svg>
--- /dev/null
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.59 8.29504L12 12.875L7.41 8.29504L6 9.70504L12 15.705L18 9.70504L16.59 8.29504Z" fill="black"/>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M184-692q-14-15-19-34t-5-39q0-47.917 33.25-81.458Q226.5-880 274-880t80.75 33.542Q388-812.917 388-765q0 20-5 39t-19 34H184ZM396-80q-63.938 0-109.469-45Q241-170 241-235h-20q-6 0-10.125-3.889T206-249l-36-369q-2-13.5 7.25-23.25T200-651h148q13.5 0 22.75 9.75T378-618l-36 369q-.75 6.222-4.875 10.111Q333-235 327-235h-26q0 39 27.867 67 27.868 28 67 28Q435-140 462.5-167.906 490-195.812 490-235v-490q0-65 45-110t110-45q65 0 110 45t45 110v615q0 12.75-8.675 21.375Q782.649-80 769.825-80 757-80 748.5-88.625T740-110v-615q0-39.188-27.867-67.094-27.867-27.906-67-27.906Q606-820 578-792.094 550-764.188 550-725v490q0 65-45.237 110Q459.525-80 396-80ZM261-295h26l28-296h-82l28 296Zm26-296h-54 82-28Z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#009e49" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#ce1126" d="M0 0h171v342H0z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#10338c" d="M0 0h513v342H0z"/><g fill="#FFF"><path d="M222.2 170.7c.3-.3.5-.6.8-.9-.2.3-.5.6-.8.9zM188 212.6l11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zM385.9 241.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM337.3 125.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zM385.9 58.9l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zM428.4 108.6l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"/><path d="M254.8 0v30.6l-45.1 25.1h45.1V115h-59.1l59.1 32.8v22.9h-26.7l-73.5-40.9v40.9H99v-48.6l-87.4 48.6H-1.2v-30.6L44 115H-1.2V55.7h59.1L-1.2 22.8V0h26.7L99 40.8V0h55.6v48.6L242.1 0z"/></g><path fill="#D80027" d="M142.8 0h-32v69.3h-112v32h112v69.4h32v-69.4h112v-32h-112z"/><path fill="#0052B4" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><path fill="#FFF" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><g fill="#D80027"><path d="m154.6 115 100.2 55.7v-15.8L183 115zM70.7 115l-71.9 39.9v15.8L99 115z"/></g><path fill="#0052B4" d="M99 55.7-1.2 0v15.7l71.9 40z"/><path fill="#FFF" d="M99 55.7-1.2 0v15.7l71.9 40z"/><g fill="#D80027"><path d="M99 55.7-1.2 0v15.7l71.9 40zM183 55.7l71.8-40V0L154.6 55.7z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fdda25" d="M0 0h513v342H0z"/><path d="M0 0h171v342H0z"/><path fill="#ef3340" d="M342 0h171v342H342z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#009b3a" d="M0 0h513v342H0z"/><path fill="#fedf00" d="m256.5 19.3 204.9 151.4L256.5 322 50.6 170.7z"/><circle fill="#FFF" cx="256.5" cy="171" r="80.4"/><path fill="#002776" d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zM334.9 186c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="red"><path d="M0 0h142v342H0zM371 0h142v342H371zM306.5 206l50.4-25.2-25.2-12.6V143l-50.4 25.2 25.2-50.4h-25.2L256.1 80l-25.2 37.8h-25.2l25.2 50.4-50.4-25.2v25.2l-25.2 12.6 50.4 25.2-12.6 25.2h50.4V269h25.2v-37.8h50.4z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 513 342"><path fill="red" d="M0 85.337h513v342H0z"/><path fill="#FFF" d="M356.174 222.609h-66.783v-66.783h-66.782v66.783h-66.783v66.782h66.783v66.783h66.782v-66.783h66.783z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg viewBox="0 0.5 21 14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M0 0h21v15H0z"/><path fill="#ee1c25" d="M0 0h21v15H0z"/><path d="M12 7.19c-.798-.5-1 .409-1 0 0-.828.895-1.5 2-1.5s2 .672 2 1.5c-.949 0-1.044.5-1.5.5-.56 0-.702 0-1.5-.5zM13.25 7a.25.25 0 1 0 0-.5.25.25 0 0 0 0 .5zm-1.81 1.962c.228-.913-.698-.824-.31-.95.788-.257 1.703.387 2.045 1.438.341 1.05-.021 2.11-.809 2.366-.293-.903-.798-.838-.939-1.272-.173-.533-.217-.668.012-1.582zm.566 1.13a.25.25 0 1 0 .476-.154.25.25 0 0 0-.476.154zM9.58 8.977c.94-.065.57-.919.81-.588.486.67.157 1.74-.737 2.389-.894.65-2.013.632-2.5-.038.768-.558.55-1.018.92-1.286.453-.33.568-.413 1.507-.477zm-.899.888a.25.25 0 1 0 .294.405.25.25 0 0 0-.294-.405zm.312-2.652c.351.874 1.049.258.809.588-.487.67-1.606.687-2.5.038-.894-.65-1.223-1.719-.736-2.39.767.559 1.138.21 1.507.478.453.33.568.413.92 1.286zm-1.124-.58a.25.25 0 1 0-.293.404.25.25 0 0 0 .293-.404zm2.619-.524c-.722.605.08 1.078-.309.951-.788-.256-1.15-1.315-.809-2.365.342-1.05 1.257-1.695 2.045-1.439-.293.903.153 1.147.012 1.581-.173.533-.217.668-.939 1.272zm.205-1.247a.25.25 0 1 0-.475-.155.25.25 0 0 0 .475.155z" fill="#FFF"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512v341.333H0z"/><path fill="#E00" d="M0 85.333h512V256H0z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M341.334 85.33H0v341.332h512V85.33z"/><path fill="#6DA544" d="M0 85.333h170.663V426.67H0z"/><path fill="#D80027" d="M341.337 85.333H512V426.67H341.337z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><circle fill="#D80027" cx="256" cy="255.994" r="96"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFDA44" d="M0 85.331h512v341.326H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path fill="#D80027" d="M0 85.337h512V256H0z"/><g fill="#FFF"><path d="M83.478 170.666c0-24.865 17.476-45.637 40.812-50.734a52.059 52.059 0 0 0-11.13-1.208c-28.688 0-51.942 23.254-51.942 51.941s23.255 51.942 51.942 51.942c3.822 0 7.543-.425 11.13-1.208-23.336-5.095-40.812-25.867-40.812-50.733zM150.261 122.435l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921z"/><path d="m121.344 144.696 3.683 11.337h11.921l-9.645 7.007 3.684 11.337-9.643-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921zM179.178 144.696l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.685-11.337-9.645-7.007h11.921zM168.047 178.087l3.684 11.337h11.921l-9.644 7.007 3.684 11.337-9.645-7.006-9.643 7.006 3.684-11.337-9.644-7.007h11.92zM132.474 178.087l3.683 11.337h11.921l-9.644 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.684-11.337-9.644-7.007h11.92z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.337h512v341.326H0z"/><path fill="#0052B4" d="M0 85.337h256V256H0z"/><path fill="#FFF" d="M186.435 170.669 162.558 181.9l12.714 23.125-25.927-4.961-3.286 26.192L128 206.993l-18.06 19.263-3.285-26.192-25.927 4.96 12.714-23.125-23.877-11.23 23.877-11.231-12.714-23.125 25.927 4.96 3.286-26.192L128 134.344l18.06-19.263 3.285 26.192 25.928-4.96-12.715 23.125z"/><circle fill="#0052B4" cx="128" cy="170.674" r="29.006"/><path fill="#FFF" d="M128 190.06c-10.692 0-19.391-8.7-19.391-19.391 0-10.692 8.7-19.391 19.391-19.391 10.692 0 19.391 8.7 19.391 19.391 0 10.691-8.699 19.391-19.391 19.391z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#D80027"><path d="M0 0h513v26.3H0zM0 52.6h513v26.3H0zM0 105.2h513v26.3H0zM0 157.8h513v26.3H0zM0 210.5h513v26.3H0zM0 263.1h513v26.3H0zM0 315.7h513V342H0z"/></g><path fill="#2E52B2" d="M0 0h256.5v184.1H0z"/><g fill="#FFF"><path d="m47.8 138.9-4-12.8-4.4 12.8H26.2l10.7 7.7-4 12.8 10.9-7.9 10.6 7.9-4.1-12.8 10.9-7.7zM104.1 138.9l-4.1-12.8-4.2 12.8H82.6l10.7 7.7-4 12.8 10.7-7.9 10.8 7.9-4-12.8 10.7-7.7zM160.6 138.9l-4.3-12.8-4 12.8h-13.5l11 7.7-4.2 12.8 10.7-7.9 11 7.9-4.2-12.8 10.7-7.7zM216.8 138.9l-4-12.8-4.2 12.8h-13.3l10.8 7.7-4 12.8 10.7-7.9 10.8 7.9-4.3-12.8 11-7.7zM100 75.3l-4.2 12.8H82.6L93.3 96l-4 12.6 10.7-7.8 10.8 7.8-4-12.6 10.7-7.9h-13.4zM43.8 75.3l-4.4 12.8H26.2L36.9 96l-4 12.6 10.9-7.8 10.6 7.8L50.3 96l10.9-7.9H47.8zM156.3 75.3l-4 12.8h-13.5l11 7.9-4.2 12.6 10.7-7.8 11 7.8-4.2-12.6 10.7-7.9h-13.2zM212.8 75.3l-4.2 12.8h-13.3l10.8 7.9-4 12.6 10.7-7.8 10.8 7.8-4.3-12.6 11-7.9h-13.5zM43.8 24.7l-4.4 12.6H26.2l10.7 7.9-4 12.7L43.8 50l10.6 7.9-4.1-12.7 10.9-7.9H47.8zM100 24.7l-4.2 12.6H82.6l10.7 7.9-4 12.7L100 50l10.8 7.9-4-12.7 10.7-7.9h-13.4zM156.3 24.7l-4 12.6h-13.5l11 7.9-4.2 12.7 10.7-7.9 11 7.9-4.2-12.7 10.7-7.9h-13.2zM212.8 24.7l-4.2 12.6h-13.3l10.8 7.9-4 12.7 10.7-7.9 10.8 7.9-4.3-12.7 11-7.9h-13.5z"/></g></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path d="M114.024 256.001 0 141.926v228.17z"/><path fill="#ffb915" d="M161.192 256 0 94.7v47.226l114.024 114.075L0 370.096v47.138z"/><path fill="#007847" d="M509.833 289.391c.058-.44.804-.878 2.167-1.318v-65.464H222.602L85.33 85.337H0V94.7L161.192 256 0 417.234v9.429h85.33l137.272-137.272h287.231z"/><path fill="#000c8a" d="M503.181 322.783H236.433l-103.881 103.88H512v-103.88z"/><path fill="#e1392d" d="M503.181 189.217H512V85.337H132.552l103.881 103.88z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none" opacity=".1"/><path d="M12 1c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="23" height="19" viewBox="0 0 23 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7C12 8.06087 11.5786 9.07828 10.8284 9.82843C10.0783 10.5786 9.06087 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 5.93913 4.42143 4.92172 5.17157 4.17157C5.92172 3.42143 6.93913 3 8 3V3ZM8 13C10.67 13 16 14.34 16 17V19H0V17C0 14.34 5.33 13 8 13ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#000000"/>
+</svg>
--- /dev/null
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.99 2C6.47 2 2 6.48 2 12C2 17.52 6.47 22 11.99 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 11.99 2ZM18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.35 18.92 8ZM12 4.04C12.83 5.24 13.48 6.57 13.91 8H10.09C10.52 6.57 11.17 5.24 12 4.04ZM4.26 14C4.1 13.36 4 12.69 4 12C4 11.31 4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12C7.5 12.68 7.56 13.34 7.64 14H4.26ZM5.08 16H8.03C8.35 17.25 8.81 18.45 9.41 19.56C7.57 18.93 6.04 17.66 5.08 16ZM8.03 8H5.08C6.04 6.34 7.57 5.07 9.41 4.44C8.81 5.55 8.35 6.75 8.03 8ZM12 19.96C11.17 18.76 10.52 17.43 10.09 16H13.91C13.48 17.43 12.83 18.76 12 19.96ZM14.34 14H9.66C9.57 13.34 9.5 12.68 9.5 12C9.5 11.32 9.57 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.68 14.43 13.34 14.34 14ZM14.59 19.56C15.19 18.45 15.65 17.25 15.97 16H18.92C17.96 17.65 16.43 18.93 14.59 19.56ZM16.36 14C16.44 13.34 16.5 12.68 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.69 19.9 13.36 19.74 14H16.36Z" fill="black"/>
+</svg>
--- /dev/null
+<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 1.27L2.28 0L21 18.72L19.73 20L15.73 16C15.9 16.31 16 16.64 16 17V19H0V17C0 14.34 5.33 13 8 13C9.77 13 12.72 13.59 14.5 14.77L10.12 10.39C9.5 10.78 8.78 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 6.22 4.22 5.5 4.61 4.88L1 1.27ZM8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7V7.17L7.83 3H8ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#9C0707"/>
+</svg>
--- /dev/null
+<svg width="50" height="93" viewBox="0 0 50 93" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7628 73.8622C11.7925 73.9104 11.8196 73.9602 11.8441 74.0113L14.6714 78.9066C16.8218 82.5868 20.3392 85.2682 24.4576 86.3667C28.576 87.4652 32.9617 86.8917 36.6592 84.7713L36.7948 84.6967C40.4339 82.5266 43.0758 79.0148 44.1522 74.9168C45.2287 70.8188 44.6538 66.462 42.5511 62.7835L24.55 31.5952C23.4582 31.5595 22.4078 31.169 21.5579 30.4828C20.708 29.7966 20.1049 28.8521 19.84 27.7924C19.5751 26.7327 19.6627 25.6155 20.0897 24.61C20.5166 23.6046 21.2597 22.7657 22.2062 22.2204C23.1527 21.6752 24.2511 21.4533 25.3351 21.5883C26.419 21.7233 27.4295 22.208 28.2133 22.9688C28.9971 23.7296 29.5116 24.7251 29.6788 25.8046C29.8461 26.8841 29.6569 27.9886 29.1401 28.9509L47.148 60.1393C49.9521 65.0155 50.726 70.799 49.3027 76.2409C47.8795 81.6829 44.3731 86.3468 39.5407 89.2258C39.4104 89.3213 39.272 89.4052 39.1272 89.4767C34.2442 92.2178 28.4827 92.9402 23.0741 91.4895C17.6655 90.0389 13.0389 86.5302 10.183 81.7135C10.0968 81.587 10.0219 81.4531 9.95926 81.3135L7.24723 76.6284L7.16587 76.4996C6.84576 75.9775 6.33816 75.5975 5.747 75.4374C5.15584 75.2773 4.52584 75.3493 3.98601 75.6386C3.68462 75.8135 3.35172 75.9274 3.00632 75.9737C2.66092 76.02 2.30977 75.9978 1.97294 75.9084C1.63611 75.819 1.32019 75.6641 1.04321 75.4526C0.766237 75.2411 0.533626 74.9772 0.358668 74.6758C0.18371 74.3744 0.0698312 74.0415 0.0235283 73.6961C-0.0227746 73.3507 -0.000592648 72.9995 0.0888089 72.6627C0.17821 72.3259 0.333083 72.01 0.544578 71.733C0.756074 71.456 1.02005 71.2234 1.32144 71.0484C3.07661 70.0427 5.15809 69.7712 7.11251 70.2931C9.06693 70.8151 10.7359 72.0881 11.756 73.835L11.7628 73.8622ZM30.747 8.18523e-07C29.4452 -0.000742291 28.194 0.504523 27.2579 1.40909C26.3217 2.31366 25.7737 3.54669 25.7297 4.84775L15.6816 10.6515C15.6305 10.676 15.5808 10.7031 15.5325 10.7329C11.7676 12.916 9.0218 16.5028 7.89703 20.707C6.77225 24.9113 7.36024 29.3899 9.53211 33.1614L9.62703 33.3241L9.7084 33.4597L30.6249 69.6924C30.9206 70.2333 30.9936 70.8682 30.8283 71.462C30.6738 72.056 30.2913 72.5652 29.7639 72.8791C29.5615 72.992 29.3766 73.1336 29.2147 73.2994V73.2994C28.7619 73.7725 28.4992 74.3958 28.4767 75.0503C28.4542 75.7047 28.6734 76.3446 29.0927 76.8476C29.5119 77.3507 30.1017 77.6818 30.7495 77.7777C31.3973 77.8736 32.0577 77.7275 32.6047 77.3675C34.2343 76.341 35.4179 74.739 35.9204 72.8798C36.4229 71.0207 36.2074 69.0405 35.3168 67.333C35.2451 67.1605 35.1541 66.9968 35.0456 66.8448L14.2511 30.829L14.1697 30.6799L14.0748 30.5104C12.6177 27.9552 12.2276 24.9282 12.9893 22.0871C13.751 19.246 15.6029 16.8202 18.1428 15.3365L18.2784 15.2552L28.3197 9.45822C28.992 9.81663 29.7371 10.0172 30.4985 10.0448C31.2599 10.0723 32.0176 9.92613 32.714 9.61725C33.4105 9.30837 34.0275 8.84494 34.5182 8.2621C35.0088 7.67927 35.3604 6.99235 35.546 6.25343C35.7317 5.51451 35.7466 4.743 35.5897 3.99745C35.4328 3.2519 35.1081 2.55189 34.6404 1.95049C34.1726 1.3491 33.574 0.862131 32.8901 0.526529C32.2061 0.190926 31.4546 0.0154886 30.6927 0.0135535L30.747 8.18523e-07Z" fill="#F21B1B"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 13 12" width="13" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M 7.543 12 L 7.543 10.937 C 8.651 10.617 9.557 10.003 10.26 9.094 C 10.963 8.186 11.314 7.154 11.314 6 C 11.314 4.846 10.603 3.712 9.906 2.798 C 9.209 1.884 8.663 1.371 7.543 1.063 L 7.543 0 C 8.96 0.32 10.114 1.037 11.006 2.151 C 11.897 3.266 12.343 4.549 12.343 6 C 12.343 7.451 11.897 8.734 11.006 9.849 C 10.114 10.963 8.96 11.68 7.543 12 Z M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
+</svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="624" height="750" viewBox="0 0 624 750" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M614.44 570.333C601.997 557.891 583.331 557.891 570.883 570.333L499.331 641.891V32.104C499.331 14.9947 485.331 0.99469 468.221 0.99469C451.112 0.99469 437.112 14.9947 437.112 32.104V641.877L365.555 570.32C353.112 557.877 334.445 557.877 321.997 570.32C309.555 582.763 309.555 601.429 321.997 613.877L446.44 738.32C447.997 739.877 449.549 741.429 451.107 742.987L452.664 744.544C454.221 744.544 454.221 746.101 455.773 746.101C457.331 746.101 457.331 746.101 458.883 747.659C460.44 747.659 460.44 747.659 461.992 749.216H468.216H474.44C475.997 749.216 475.997 749.216 477.549 747.659C479.107 747.659 479.107 747.659 480.659 746.101C482.216 746.101 482.216 744.544 483.768 744.544C483.768 744.544 485.325 744.544 485.325 742.987C486.883 741.429 488.435 739.877 489.992 738.32L614.435 613.877C626.888 601.435 626.888 582.768 614.44 570.325L614.44 570.333Z" fill="black"/>
+<path d="M303.333 134.773L174.224 5.664C172.667 5.664 172.667 4.10667 171.115 4.10667C169.557 4.10667 169.557 2.54934 168.005 2.54934C166.448 2.54934 166.448 2.54934 164.896 0.992004H161.787C158.667 0.997213 155.557 0.997213 150.891 0.997213H147.781C146.224 0.997213 146.224 0.997213 144.672 2.55455C143.115 2.55455 143.115 4.11188 141.563 4.11188C140.005 4.11188 140.005 5.66921 138.453 5.66921L9.34413 134.779C-3.09854 147.221 -3.09854 165.888 9.34413 178.336C21.7868 190.779 40.4535 190.779 52.9015 178.336L126 106.773V716.56C126 733.669 140 747.669 157.109 747.669C174.219 747.669 188.219 733.669 188.219 716.56L188.224 106.773L259.781 178.331C266 184.555 273.776 187.664 281.557 187.664C289.333 187.664 297.115 184.555 303.333 178.331C315.776 165.888 315.776 147.221 303.333 134.773V134.773Z" fill="black"/>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M444-164q-27-11-40-41t2-59q8-15 40-80.5t70-145q38-79.5 74.5-155T643-754q3-7 10.5-10t15.5-1q8 2 13 9t3 15q-9 36-29.5 119T613-451.5q-22 87.5-41 159T547-206q-12 30-43 41.5t-60 .5Zm484-393q-13 13-31.5 13T864-556q-34-29-71-54t-71-41l23-90q54 24 98.5 55t84.5 66q14 12 14 30.5T928-557Zm-896 0q-13-13-13.5-32T32-620q92-84 207-132t241-48q24 0 54.5 2.5T594-790l-42 85q-16-2-34-3.5t-38-1.5q-108 0-204.5 41.5T96-556q-14 12-32.5 12T32-557Zm727 169q-13 13-30.5 13T696-387q-11-8-19-14t-14-11l22-92q16 9 34.5 22.5T759-450q14 12 14 30t-14 32Zm-558 0q-14-14-13-32.5t13-29.5q61-53 129-81.5T483-560l-45 93q-49 6-92.5 27T264-387q-15 12-32.5 12T201-388Z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 4H8.5V3C8.5 1.62 7.38 0.5 6 0.5C4.62 0.5 3.5 1.62 3.5 3V4H3C2.45 4 2 4.45 2 5V10C2 10.55 2.45 11 3 11H9C9.55 11 10 10.55 10 10V5C10 4.45 9.55 4 9 4ZM4.45 3C4.45 2.145 5.145 1.45 6 1.45C6.855 1.45 7.55 2.145 7.55 3V4H4.45V3ZM8 8H6.5V9.5H5.5V8H4V7H5.5V5.5H6.5V7H8V8Z" fill="#FAFBFB"/>
+</svg>
--- /dev/null
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 1C3.24 1 1 3.24 1 6C1 8.76 3.24 11 6 11C8.76 11 11 8.76 11 6C11 3.24 8.76 1 6 1ZM5.5 9.965C3.525 9.72 2 8.04 2 6C2 5.69 2.04 5.395 2.105 5.105L4.5 7.5V8C4.5 8.55 4.95 9 5.5 9V9.965ZM8.95 8.695C8.82 8.29 8.45 8 8 8H7.5V6.5C7.5 6.225 7.275 6 7 6H4V5H5C5.275 5 5.5 4.775 5.5 4.5V3.5H6.5C7.05 3.5 7.5 3.05 7.5 2.5V2.295C8.965 2.89 10 4.325 10 6C10 7.04 9.6 7.985 8.95 8.695Z" fill="#FAFBFB"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 10.05 12" width="10.05" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
+</svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
+<path d="M12 20q-3.35 0-5.675-2.325Q4 15.35 4 12q0-3.35 2.325-5.675Q8.65 4 12 4q1.725 0 3.3.713 1.575.712 2.7 2.037V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2Q13.625 6 12 6 9.5 6 7.75 7.75T6 12q0 2.5 1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325Q14.75 20 12 20Z"/>
+</svg>
\ No newline at end of file
--- /dev/null
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 13.5C9.67 13.5 7.69 14.96 6.89 17H17.11C16.31 14.96 14.33 13.5 12 13.5ZM7.82 12L8.88 10.94L9.94 12L11 10.94L9.94 9.88L11 8.82L9.94 7.76L8.88 8.82L7.82 7.76L6.76 8.82L7.82 9.88L6.76 10.94L7.82 12ZM11.99 2C6.47 2 2 6.47 2 12C2 17.53 6.47 22 11.99 22C17.51 22 22 17.53 22 12C22 6.47 17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20ZM16.18 7.76L15.12 8.82L14.06 7.76L13 8.82L14.06 9.88L13 10.94L14.06 12L15.12 10.94L16.18 12L17.24 10.94L16.18 9.88L17.24 8.82L16.18 7.76Z" fill="black"/>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M20.3642 8.55102L19.1342 10.401C19.7274 11.5841 20.0178 12.8958 19.9794 14.2187C19.941 15.5416 19.575 16.8343 18.9142 17.981H5.05424C4.19541 16.4911 3.83954 14.7641 4.03938 13.0561C4.23923 11.348 4.98415 9.74984 6.16372 8.49844C7.34329 7.24705 8.89471 6.40906 10.588 6.10871C12.2813 5.80837 14.0262 6.06165 15.5642 6.83102L17.4142 5.60102C15.5307 4.39323 13.2966 3.85202 11.0691 4.06395C8.8417 4.27588 6.74969 5.22871 5.12773 6.77003C3.50578 8.31134 2.4476 10.3521 2.12246 12.5658C1.79732 14.7796 2.22399 17.0384 3.33424 18.981C3.50875 19.2833 3.75933 19.5346 4.06107 19.7101C4.36282 19.8855 4.70521 19.9789 5.05424 19.981H18.9042C19.2567 19.9824 19.6032 19.8907 19.9087 19.7151C20.2143 19.5395 20.468 19.2862 20.6442 18.981C21.5656 17.3849 22.028 15.5653 21.9804 13.723C21.9327 11.8807 21.3769 10.0873 20.3742 8.54102L20.3642 8.55102Z" fill="black"/>
+<path d="M10.5742 15.391C10.76 15.577 10.9806 15.7245 11.2234 15.8251C11.4662 15.9258 11.7264 15.9776 11.9892 15.9776C12.2521 15.9776 12.5123 15.9258 12.7551 15.8251C12.9979 15.7245 13.2185 15.577 13.4042 15.391L19.0642 6.90102L10.5742 12.561C10.3883 12.7468 10.2408 12.9673 10.1401 13.2101C10.0395 13.4529 9.98767 13.7132 9.98767 13.976C9.98767 14.2388 10.0395 14.4991 10.1401 14.7419C10.2408 14.9847 10.3883 15.2053 10.5742 15.391Z" fill="black"/>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 17.27l5.17 3.12c.38.23.85-.11.75-.54l-1.37-5.88 4.56-3.95c.33-.29.16-.84-.29-.88l-6.01-.51-2.35-5.54c-.17-.41-.75-.41-.92 0L9.19 8.63l-6.01.51c-.44.04-.62.59-.28.88l4.56 3.95-1.37 5.88c-.1.43.37.77.75.54L12 17.27z"/></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#000000"><path d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z" /></svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H3V5h18v11z"/></svg>
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 virtualstudio.cpp
+ * \author Matt Horton, based on code by Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "virtualstudio.h"
+
+#include <QtWebEngineQuick/qtwebenginequickglobal.h>
+
+#include <QDebug>
+#include <QDesktopServices>
+#include <QDir>
+#include <QFile>
+#include <QFontDatabase>
+#include <QMessageBox>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickStyle>
+#include <QQuickView>
+#include <QSGRendererInterface>
+#include <QSettings>
+#include <QSslSocket>
+#include <QStandardPaths>
+#include <QSysInfo>
+#include <QTextStream>
+#include <QtGlobal>
+#include <algorithm>
+#include <iostream>
+
+// TODO: remove me; including this to work-around this bug
+// https://bugreports.qt.io/browse/QTBUG-55199
+#include <QSvgGenerator>
+
+#include "../JackTrip.h"
+#include "../Settings.h"
+#include "../jacktrip_globals.h"
+#include "JTApplication.h"
+#include "WebSocketTransport.h"
+#include "vsApi.h"
+#include "vsAudio.h"
+#include "vsAuth.h"
+#include "vsDeeplink.h"
+#include "vsDevice.h"
+#include "vsQmlClipboard.h"
+#include "vsWebSocket.h"
+
+#ifdef _WIN32
+#include <wingdi.h>
+#endif
+
+#if defined(JACKTRIP_BUILD_INFO)
+#define STR(s) #s
+#define TO_STRING(s) STR(s)
+const QString jackTripBuildInfo = QLatin1String(TO_STRING(JACKTRIP_BUILD_INFO));
+#else
+const QString jackTripBuildInfo = QLatin1String("");
+#endif
+
+static QTextStream* ts;
+static QFile outFile;
+
+void qtMessageHandler([[maybe_unused]] QtMsgType type,
+ [[maybe_unused]] const QMessageLogContext& context,
+ const QString& msg)
+{
+ std::cerr << msg.toStdString() << std::endl;
+ // Writes to file in order to debug bundles and executables
+ *ts << msg << Qt::endl;
+}
+
+VirtualStudio::VirtualStudio(UserInterface& parent)
+ : QObject()
+ , m_interface(parent)
+ , m_view(new VsQuickView)
+ , m_audioConfigPtr(
+ new VsAudio(this)) // this needs to be constructed before loadSettings()
+{
+ // load or initialize persisted settings
+ loadSettings();
+
+ // TODO: remove me; this is a hack for this bug
+ // https://bugreports.qt.io/browse/QTBUG-55199
+ QSvgGenerator svgImageHack;
+
+ // use a singleton 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_networkAccessManagerPtr));
+ m_api->setApiHost(PROD_API_HOST);
+ if (m_testMode) {
+ m_api->setApiHost(TEST_API_HOST);
+ }
+
+ // instantiate auth
+ 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, [=]() {
+ m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
+ });
+ connect(m_auth.data(), &VsAuth::fetchUserInfoFailed, this, [=]() {
+ m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
+ });
+ connect(m_auth.data(), &VsAuth::deviceCodeExpired, this, [=]() {
+ m_auth->authenticate(QStringLiteral("")); // retry without using refresh token
+ });
+
+ m_webChannelServer.reset(new QWebSocketServer(
+ QStringLiteral("Qt6 Virtual Studio Server"), QWebSocketServer::NonSecureMode));
+ connect(m_webChannelServer.data(), &QWebSocketServer::newConnection, this, [=]() {
+ m_webChannel->connectTo(
+ new WebSocketTransport(m_webChannelServer->nextPendingConnection()));
+ });
+
+ m_webChannel.reset(new QWebChannel());
+ m_webChannel->registerObject(QStringLiteral("virtualstudio"), this);
+
+ // Load our font for our qml interface
+ QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf"));
+ QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf"));
+
+ // Set our font scaling to convert points to pixels
+ m_fontScale = float(4.0 / 3.0);
+
+ // Initialize timer needed for network outage indicator
+ m_networkOutageTimer.setTimerType(Qt::CoarseTimer);
+ m_networkOutageTimer.setSingleShot(true);
+ m_networkOutageTimer.setInterval(5000);
+ m_networkOutageTimer.callOnTimeout([&]() {
+ if (m_devicePtr.isNull())
+ return;
+ m_devicePtr->setNetworkOutage(false);
+ emit updatedNetworkOutage(false);
+ });
+
+ // register QML types
+ qmlRegisterType<VsServerInfo>("org.jacktrip.jacktrip", 1, 0, "VsServerInfo");
+
+ // Register clipboard Qml type
+ qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
+
+ // setup QML view
+ m_view->engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
+ this);
+ m_view->engine()->rootContext()->setContextProperty(QStringLiteral("auth"),
+ m_auth.get());
+ m_view->engine()->rootContext()->setContextProperty(QStringLiteral("audio"),
+ m_audioConfigPtr.get());
+ m_view->engine()->rootContext()->setContextProperty(
+ QStringLiteral("permissions"),
+ QVariant::fromValue(&m_audioConfigPtr->getPermissions()));
+ m_view->setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml")));
+ m_view->setMinimumSize(QSize(800, 640));
+ // m_view->setMaximumSize(QSize(696, 577));
+ m_view->setResizeMode(QQuickView::SizeRootObjectToView);
+ m_view->resize(800 * m_uiScale, 640 * m_uiScale);
+
+ // Connect our timers
+ connect(this, &VirtualStudio::scheduleStudioRefresh, this,
+ &VirtualStudio::refreshStudios, Qt::QueuedConnection);
+ connect(&m_heartbeatTimer, &QTimer::timeout, this, &VirtualStudio::sendHeartbeat,
+ Qt::QueuedConnection);
+
+ // QueuedConnection since refreshFinished is sometimes signaled from a network reply
+ // thread
+ connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio,
+ Qt::QueuedConnection);
+
+ // handle audio config errors
+ connect(&m_audioConfigPtr->getWorker(), &VsAudioWorker::signalError, this,
+ &VirtualStudio::processError, Qt::QueuedConnection);
+
+ // when connected to server, trigger UI modal when feedback is detected
+ connect(m_audioConfigPtr.get(), &VsAudio::feedbackDetected, this,
+ &VirtualStudio::detectedFeedbackLoop, Qt::QueuedConnection);
+
+ // call exit() when the UI window is closed
+ connect(m_view.get(), &VsQuickView::windowClose, this, &VirtualStudio::exit,
+ Qt::QueuedConnection);
+
+ // prepare handler for deeplinks jacktrip://join/<StudioID>
+ m_deepLinkPtr.reset(new VsDeeplink(m_interface.getSettings().getDeeplink()));
+ if (!m_deepLinkPtr->getDeeplink().isEmpty()) {
+ bool readyForExit = m_deepLinkPtr->waitForReady();
+ if (readyForExit)
+ std::exit(0);
+ }
+
+ // Log to file
+ QString logPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+ QDir logDir;
+ if (!logDir.exists(logPath)) {
+ logDir.mkpath(logPath);
+ }
+ QString fileLoc(logPath.append("/log.txt"));
+ qDebug() << "Log file location:" << fileLoc;
+ outFile.setFileName(fileLoc);
+ if (!outFile.open(QIODevice::WriteOnly | QIODevice::Append)) {
+ qDebug() << "Log file open failed:" << outFile.errorString();
+ }
+ ts = new QTextStream(&outFile);
+ qInstallMessageHandler(qtMessageHandler);
+
+ // Get ready for deep link signals
+ QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalDeeplink, this,
+ &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection);
+ m_deepLinkPtr->readyForSignals();
+}
+
+void VirtualStudio::show()
+{
+ if (m_checkSsl) {
+ // Check our available SSL version
+ QString sslVersion = QSslSocket::sslLibraryVersionString();
+ // Important: this needs to be output with qDebug rather than to std::cout
+ // otherwise it may get passed to an existing JackTrip instance in place of our
+ // deeplink. (Need to find the root cause of this.)
+ qDebug() << "SSL Library: " << sslVersion;
+ if (sslVersion.isEmpty()) {
+ QMessageBox msgBox;
+ msgBox.setText(
+ QStringLiteral("OpenSSL was not found. You will not be able to connect "
+ "to the Virtual Studio server."));
+ msgBox.setWindowTitle(QStringLiteral("SSL Error"));
+ msgBox.exec();
+ }
+ m_checkSsl = false;
+ }
+
+ 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 support.jacktrip.com for possible solutions.");
+ msgBox.setWindowTitle(QStringLiteral("JackTrip Is Missing QML Modules"));
+ connect(&msgBox, &QMessageBox::finished, this, &VirtualStudio::toClassicMode,
+ Qt::QueuedConnection);
+ msgBox.exec();
+ return;
+ }
+
+ raiseToTop();
+}
+
+void VirtualStudio::hide()
+{
+ if (!m_view.isNull())
+ m_view->hide();
+}
+
+void VirtualStudio::raiseToTop()
+{
+ if (m_view->status() != QQuickView::Ready)
+ return;
+ m_view->show(); // Restore from systray
+ m_view->raise(); // raise to top
+ m_view->requestActivate(); // focus on window
+}
+
+int VirtualStudio::webChannelPort()
+{
+ return m_webChannelPort;
+}
+
+bool VirtualStudio::hasRefreshToken()
+{
+ return !m_refreshToken.isEmpty();
+}
+
+QString VirtualStudio::versionString()
+{
+ return QLatin1String(gVersion);
+}
+
+QString VirtualStudio::buildString()
+{
+ QString result;
+ if (!jackTripBuildInfo.isEmpty()) {
+ result += "Build ";
+ result += jackTripBuildInfo;
+ result += "<br/>\n";
+ }
+ result += "Qt version ";
+ result += QT_VERSION_STR;
+#ifdef QT_OPENSOURCE
+ result += " (open source)";
+#else
+ result += " (commercial)";
+#endif
+ return result;
+}
+
+QString VirtualStudio::copyrightString()
+{
+ QString result;
+ bool gplLicense = false;
+#ifdef QT_OPENSOURCE
+ gplLicense = true;
+#endif
+
+ result +=
+ "Copyright © 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al. SoundWIRE "
+ "group at CCRMA, Stanford University.<br/><br/>\n";
+ result +=
+ "Virtual Studio interface and integration Copyright © 2022-2024 JackTrip "
+ "Labs, Inc.<br/><br/>\n";
+
+ if (hasClassicMode()) {
+ gplLicense = true;
+ result +=
+ "Classic mode graphical user interface component originally released as "
+ "QJackTrip, Copyright © 2020 Aaron Wyatt.<br/><br/>\n";
+ }
+
+ if (m_audioConfigPtr->asioIsAvailable()) {
+ result +=
+ "This build of JackTrip includes support for ASIO. ASIO is a trademark and "
+ "software of Steinberg Media Technologies GmbH.<br/><br/>";
+ }
+
+ result +=
+ "This app is free and open source software provided "as is" under the ";
+ result += (gplLicense ? "GPL" : "MIT");
+ result += " license, without warranty of any kind.\n";
+ result += "See the included LICENSE.md file for more information.<br/><br/>\n";
+
+ return result;
+}
+
+QString VirtualStudio::logoSection()
+{
+ return m_logoSection;
+}
+
+QString VirtualStudio::connectedErrorMsg()
+{
+ return m_connectedErrorMsg;
+}
+
+void VirtualStudio::setConnectedErrorMsg(const QString& msg)
+{
+ if (m_connectedErrorMsg == msg)
+ return;
+ m_connectedErrorMsg = msg;
+ emit connectedErrorMsgChanged();
+}
+
+bool VirtualStudio::networkOutage()
+{
+ return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage();
+}
+
+QJsonObject VirtualStudio::regions()
+{
+ return m_regions;
+}
+
+QJsonObject VirtualStudio::userMetadata()
+{
+ return m_userMetadata;
+}
+
+QString VirtualStudio::connectionState()
+{
+ return m_connectionState;
+}
+
+QJsonObject VirtualStudio::networkStats()
+{
+ return m_networkStats;
+}
+
+QString VirtualStudio::updateChannel()
+{
+ return m_updateChannel;
+}
+
+void VirtualStudio::setUpdateChannel(const QString& channel)
+{
+ if (m_updateChannel == channel)
+ return;
+ m_updateChannel = channel;
+ emit updateChannelChanged();
+}
+
+bool VirtualStudio::showInactive()
+{
+ return m_showInactive;
+}
+
+void VirtualStudio::setShowInactive(bool inactive)
+{
+ if (m_showInactive == inactive)
+ return;
+ m_showInactive = inactive;
+ emit showInactiveChanged();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("ShowInactive"), m_showInactive);
+ settings.endGroup();
+}
+
+bool VirtualStudio::showSelfHosted()
+{
+ return m_showSelfHosted;
+}
+
+void VirtualStudio::setShowSelfHosted(bool selfHosted)
+{
+ if (m_showSelfHosted == selfHosted)
+ return;
+ m_showSelfHosted = selfHosted;
+ emit showSelfHostedChanged();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted);
+ settings.endGroup();
+}
+
+bool VirtualStudio::showDeviceSetup()
+{
+ return m_showDeviceSetup;
+}
+
+void VirtualStudio::setShowDeviceSetup(bool show)
+{
+ if (m_showDeviceSetup == show)
+ return;
+ m_showDeviceSetup = show;
+ emit showDeviceSetupChanged();
+}
+
+QString VirtualStudio::windowState()
+{
+ return m_windowState;
+}
+
+void VirtualStudio::setWindowState(QString state)
+{
+ if (m_windowState == state)
+ return;
+ m_windowState = state;
+ // refresh studio list if navigating to browse window
+ // only if user id is empty (edge case for when logging in)
+ if (m_windowState == "browse" && !m_userId.isEmpty()) {
+ // schedule studio refresh instead of doing it now
+ // just to reduce risk of running into a deadlock
+ emit scheduleStudioRefresh(-1, false);
+ }
+ emit windowStateUpdated();
+}
+
+QString VirtualStudio::apiHost()
+{
+ return m_apiHost;
+}
+
+void VirtualStudio::setApiHost(QString host)
+{
+ if (m_apiHost == host)
+ return;
+ m_apiHost = host;
+ emit apiHostChanged();
+}
+
+bool VirtualStudio::vsFtux()
+{
+ return m_vsFtux;
+}
+
+bool VirtualStudio::isExiting()
+{
+ return m_isExiting;
+}
+
+bool VirtualStudio::refreshInProgress()
+{
+ return m_refreshInProgress;
+}
+
+void VirtualStudio::setRefreshInProgress(bool b)
+{
+ if (m_refreshInProgress == b)
+ return;
+ m_refreshInProgress = b;
+ emit refreshInProgressChanged();
+}
+
+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);
+ 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());
+ return;
+}
+
+bool VirtualStudio::showWarnings()
+{
+ return m_showWarnings;
+}
+
+void VirtualStudio::setShowWarnings(bool show)
+{
+ if (m_showWarnings == show)
+ return;
+ m_showWarnings = show;
+ emit showWarningsChanged();
+}
+
+float VirtualStudio::fontScale()
+{
+ return m_fontScale;
+}
+
+float VirtualStudio::uiScale()
+{
+ return m_uiScale;
+}
+
+void VirtualStudio::setUiScale(float scale)
+{
+ if (scale == m_uiScale)
+ return;
+ m_uiScale = scale;
+ emit uiScaleChanged();
+}
+
+bool VirtualStudio::darkMode()
+{
+ return m_darkMode;
+}
+
+void VirtualStudio::setDarkMode(bool dark)
+{
+ if (dark == m_darkMode)
+ return;
+ m_darkMode = dark;
+ emit darkModeChanged();
+}
+
+bool VirtualStudio::collapseDeviceControls()
+{
+ return m_collapseDeviceControls;
+}
+
+void VirtualStudio::setCollapseDeviceControls(bool collapseDeviceControls)
+{
+ if (m_collapseDeviceControls == collapseDeviceControls)
+ return;
+ m_collapseDeviceControls = collapseDeviceControls;
+ emit collapseDeviceControlsChanged(collapseDeviceControls);
+}
+
+bool VirtualStudio::testMode()
+{
+ return m_testMode;
+}
+
+void VirtualStudio::setTestMode(bool test)
+{
+ if (m_testMode == test)
+ return;
+
+ QString userEmail = m_userMetadata[QStringLiteral("email")].toString();
+ if (m_userMetadata.isEmpty() || userEmail == ""
+ || !userEmail.endsWith("@jacktrip.org")) {
+ qDebug() << "Not allowed";
+ return;
+ }
+
+ // deregister app
+ if (!m_devicePtr.isNull()) {
+ m_devicePtr->removeApp();
+ m_devicePtr->disconnect();
+ m_devicePtr.reset();
+ }
+
+ m_testMode = test;
+
+ // clear existing auth state
+ m_auth->logout();
+
+ // Clear existing registers - any existing instance data will be overwritten
+ // when m_auth->authenticate finishes and slotAuthSucceeded() is called again
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("TestMode"), m_testMode);
+ settings.remove(QStringLiteral("RefreshToken"));
+ settings.remove(QStringLiteral("UserId"));
+ settings.endGroup();
+
+ // stop timers, clear data, etc.
+ resetState();
+
+ // clear user data
+ m_userMetadata = QJsonObject();
+ m_userId.clear();
+
+ // re-run authentication. This should not require another browser flow since
+ // we're starting with the existing refresh token
+ m_auth->authenticate(m_refreshToken);
+ emit testModeChanged();
+}
+
+QString VirtualStudio::studioToJoin()
+{
+ return m_studioToJoin;
+}
+
+void VirtualStudio::setStudioToJoin(const QString& id)
+{
+ if (m_studioToJoin == id)
+ return;
+ m_studioToJoin = id;
+ emit studioToJoinChanged();
+}
+
+bool VirtualStudio::noUpdater()
+{
+#ifdef NO_UPDATER
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool VirtualStudio::psiBuild()
+{
+#ifdef PSI
+ return true;
+#else
+ return false;
+#endif
+}
+
+QString VirtualStudio::failedMessage()
+{
+ return m_failedMessage;
+}
+
+void VirtualStudio::joinStudio()
+{
+ // nothing to do unless on setup or connected windows with studio to join
+ if ((m_windowState != "setup" && m_windowState != "connected")
+ || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty())
+ return;
+
+ // make sure we've retrieved a list of servers
+ QMutexLocker locker(&m_refreshMutex);
+ if (m_servers.isEmpty()) {
+ // No servers yet. Making sure we have them.
+ // refreshStudios emits refreshFinished which
+ // will come back to this function.
+ locker.unlock();
+ refreshStudios(-1, true);
+ return;
+ }
+
+ // pop studioToJoin
+ const QString targetId = m_studioToJoin;
+ setStudioToJoin("");
+
+ // stop audio if already running (settings or setup windows)
+ m_audioConfigPtr->stopAudio(true);
+
+ // find and populate data for current studio
+ VsServerInfoPointer sPtr;
+ for (const VsServerInfoPointer& s : m_servers) {
+ if (s->id() == targetId) {
+ sPtr = s;
+ break;
+ }
+ }
+ locker.unlock();
+
+ if (sPtr.isNull()) {
+ m_failedMessage = "Unable to find studio " + targetId;
+ emit failedMessageChanged();
+ emit failed();
+ return;
+ }
+
+ m_currentStudio = *sPtr;
+ emit currentStudioChanged();
+
+ if (m_windowState == "setup") {
+ m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate());
+ m_audioConfigPtr->startAudio();
+ return;
+ }
+
+ // m_windowState == "connected"
+ connectToStudio();
+}
+
+bool VirtualStudio::hasClassicMode()
+{
+#ifdef NO_CLASSIC
+ return false;
+#else
+ return true;
+#endif // NO_CLASSIC
+}
+
+void VirtualStudio::toVirtualStudioMode()
+{
+ QSettings settings;
+ settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_VS);
+ setWindowState(QStringLiteral("login"));
+ login();
+}
+
+void VirtualStudio::toClassicMode()
+{
+ if (hasClassicMode()) {
+ m_interface.setMode(UserInterface::MODE_CLASSIC);
+
+ QSettings settings;
+ settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_CLASSIC);
+
+ // stop timers, clear data, etc.
+ resetState();
+ setWindowState(QStringLiteral("start"));
+ m_auth->logout();
+ } else {
+ std::cerr << "JackTrip was not built with support for Classic mode." << std::endl;
+ }
+}
+
+void VirtualStudio::login()
+{
+ if (m_refreshToken.isEmpty()) {
+ m_auth->authenticate(QStringLiteral(""));
+ } else {
+ m_auth->authenticate(m_refreshToken);
+ }
+}
+
+void VirtualStudio::logout()
+{
+ // deregister app
+ if (!m_devicePtr.isNull()) {
+ m_devicePtr->removeApp();
+ m_devicePtr->disconnect();
+ m_devicePtr.reset();
+ }
+
+ QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout");
+ QUrlQuery query;
+ query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID);
+ if (m_testMode) {
+ query.addQueryItem(QStringLiteral("returnTo"),
+ QStringLiteral("https://next-test.jacktrip.com/"));
+ } else {
+ query.addQueryItem(QStringLiteral("returnTo"),
+ QStringLiteral("https://www.jacktrip.com/"));
+ }
+
+ logoutURL.setQuery(query);
+ QDesktopServices::openUrl(logoutURL);
+
+ m_auth->logout();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.remove(QStringLiteral("RefreshToken"));
+ settings.remove(QStringLiteral("UserId"));
+ settings.endGroup();
+
+ // stop timers, clear data, etc.
+ resetState();
+
+ // clear user data
+ m_refreshToken.clear();
+ m_userMetadata = QJsonObject();
+ m_userId.clear();
+ emit hasRefreshTokenChanged();
+
+ // reset window state
+ setWindowState(QStringLiteral("login"));
+ login(); // called to retrieve new code flow token
+}
+
+void VirtualStudio::loadSettings()
+{
+ QSettings settings;
+ setUpdateChannel(
+ settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower());
+
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ m_refreshToken = settings.value(QStringLiteral("RefreshToken"), "").toString();
+ m_userId = settings.value(QStringLiteral("UserId"), "").toString();
+ m_testMode = settings.value(QStringLiteral("TestMode"), false).toBool();
+ m_showInactive = settings.value(QStringLiteral("ShowInactive"), true).toBool();
+ m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), true).toBool();
+
+ // use setters to emit signals for these if they change; otherwise, the
+ // user interface will not revert back after cancelling settings changes
+ setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat());
+ setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool());
+ setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool());
+ setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool());
+ settings.endGroup();
+
+ m_audioConfigPtr->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);
+ settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup);
+ settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings);
+ settings.endGroup();
+
+ m_audioConfigPtr->saveSettings();
+}
+
+void VirtualStudio::connectToStudio()
+{
+ m_networkStats = QJsonObject();
+ emit networkStatsChanged();
+
+ m_onConnectedScreen = true;
+
+ m_studioSocketPtr.reset(new VsWebSocket(
+ QUrl(QStringLiteral("wss://%1/api/servers/%2?auth_code=%3")
+ .arg(m_api->getApiHost(), m_currentStudio.id(), m_auth->accessToken())),
+ m_auth->accessToken(), QString(), QString()));
+ connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
+ &VirtualStudio::handleWebsocketMessage);
+ connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this,
+ &VirtualStudio::restartStudioSocket);
+ m_studioSocketPtr->openSocket();
+
+ // Check if we have an address for our server
+ if (m_currentStudio.status() != "Ready") {
+ m_connectionState = QStringLiteral("Waiting...");
+ emit connectionStateChanged();
+ } else {
+ completeConnection();
+ }
+
+ m_reconnectState = ReconnectState::NOT_RECONNECTING;
+}
+
+void VirtualStudio::completeConnection()
+{
+ // sanity check
+ if (m_currentStudio.id() == ""
+ || m_currentStudio.status() == QStringLiteral("Disabled")) {
+ processError("Studio session has ended");
+ return;
+ }
+
+ // these shouldn't happen
+ if (m_currentStudio.status() != "Ready") {
+ processError("Studio session is not ready");
+ return;
+ }
+ if (m_currentStudio.host().isEmpty()) {
+ processError("Studio host is unknown");
+ return;
+ }
+
+ // always connect with audio device controls open
+ setCollapseDeviceControls(false);
+
+ m_jackTripRunning = true;
+ m_connectionState = QStringLiteral("Preparing audio...");
+ emit connectionStateChanged();
+ try {
+ bool useRtAudio = m_audioConfigPtr->getUseRtAudio();
+ std::string input = "";
+ std::string output = "";
+ int buffer_size = 0;
+ int inputMixMode = -1;
+ int baseInputChannel = 0;
+ int numInputChannels = 2;
+ int baseOutputChannel = 0;
+ int numOutputChannels = 2;
+#ifdef RT_AUDIO
+ if (useRtAudio) {
+ // pre-populate device cache and validate first, if using rtaudio
+ if (!m_audioConfigPtr->getDeviceModelsInitialized())
+ m_audioConfigPtr->refreshDevices(true);
+ // initialize jacktrip using audio settings
+ input = m_audioConfigPtr->getInputDevice().toStdString();
+ output = m_audioConfigPtr->getOutputDevice().toStdString();
+ buffer_size = m_audioConfigPtr->getBufferSize();
+ inputMixMode = m_audioConfigPtr->getInputMixMode();
+ baseInputChannel = m_audioConfigPtr->getBaseInputChannel();
+ numInputChannels = m_audioConfigPtr->getNumInputChannels();
+ baseOutputChannel = m_audioConfigPtr->getBaseOutputChannel();
+ numOutputChannels = m_audioConfigPtr->getNumOutputChannels();
+ }
+#endif
+
+ // adjust queueBuffer config setting to map to auto headroom
+ int queue_buffer = m_audioConfigPtr->getQueueBuffer();
+ if (queue_buffer <= 0) {
+ queue_buffer = -500;
+ } else {
+ queue_buffer *= -1;
+ }
+
+ // create a new JackTrip instance
+ JackTrip* jackTrip = m_devicePtr->initJackTrip(
+ useRtAudio, input, output, baseInputChannel, numInputChannels,
+ baseOutputChannel, numOutputChannels, inputMixMode, buffer_size, queue_buffer,
+ &m_currentStudio);
+ if (jackTrip == 0) {
+ processError("Could not bind port");
+ return;
+ }
+ jackTrip->setIOStatTimeout(m_interface.getSettings().getIOStatTimeout());
+ m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate());
+
+ // this passes ownership to JackTrip
+ jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip));
+
+ QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this,
+ &VirtualStudio::connectionFinished, Qt::QueuedConnection);
+ QObject::connect(jackTrip, &JackTrip::signalError, this,
+ &VirtualStudio::processError, Qt::QueuedConnection);
+ QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this,
+ &VirtualStudio::receivedConnectionFromPeer,
+ Qt::QueuedConnection);
+ QObject::connect(jackTrip, &JackTrip::signalUdpWaitingTooLong, this,
+ &VirtualStudio::udpWaitingTooLong, Qt::QueuedConnection);
+
+ m_connectionState = QStringLiteral("Connecting...");
+ emit connectionStateChanged();
+ m_devicePtr->startJackTrip(m_currentStudio);
+
+ // update device error messages and warnings based on latest results
+ // this is necessary because we may have never loaded audio settings,
+ // or the state may have changed via the connected change devices screen
+ m_audioConfigPtr->setDevicesWarningMsg(jackTrip->getDevicesWarningMsg());
+ m_audioConfigPtr->setDevicesErrorMsg(jackTrip->getDevicesErrorMsg());
+ m_audioConfigPtr->setDevicesWarningHelpUrl(jackTrip->getDevicesWarningHelpUrl());
+ m_audioConfigPtr->setDevicesErrorHelpUrl(jackTrip->getDevicesErrorHelpUrl());
+ m_audioConfigPtr->setHighLatencyFlag(jackTrip->getHighLatencyFlag());
+
+ } catch (const std::exception& e) {
+ // Let the user know what our exception was.
+ m_connectionState = QStringLiteral("JackTrip Error");
+ emit connectionStateChanged();
+
+ processError(QString::fromUtf8(e.what()));
+ return;
+ }
+
+ m_interface.enableNap();
+}
+
+void VirtualStudio::triggerReconnect(bool refresh)
+{
+ if (!m_jackTripRunning || m_devicePtr.isNull()) {
+ if (refresh)
+ m_audioConfigPtr->refreshDevices(true);
+ else
+ m_audioConfigPtr->validateDevices();
+ return;
+ }
+
+ // this needs to be synchronous to avoid both trying
+ // to use the audio interfaces at the same time
+ // note that connectionFinished() checks m_reconnectState
+ // and uses that to update audio, then reconnect
+ m_reconnectState = refresh ? ReconnectState::RECONNECTING_REFRESH
+ : ReconnectState::RECONNECTING_VALIDATE;
+ m_connectionState = QStringLiteral("Reconnecting...");
+ emit connectionStateChanged();
+
+ // keep device enabled while stopping jacktrip
+ m_devicePtr->stopJackTrip(true);
+}
+
+void VirtualStudio::disconnect()
+{
+ // stop jackrip if it's running
+ if (m_jackTripRunning) {
+ m_devicePtr->stopJackTrip(false);
+ // persist any volume level or device changes
+ m_audioConfigPtr->saveSettings();
+ }
+
+ m_connectionState = QStringLiteral("Disconnected");
+ emit connectionStateChanged();
+ setConnectedErrorMsg("");
+
+ if (m_isExiting) {
+ emit signalExit();
+ return;
+ }
+
+ // if this occurs on the setup or settings screen (for example, due to an issue with
+ // devices) then don't emit disconnected, as that would move you back to the "Browse"
+ // screen
+ if (m_onConnectedScreen) {
+ m_onConnectedScreen = false;
+ emit disconnected();
+ }
+
+ if (!m_jackTripRunning) {
+ return;
+ }
+ m_jackTripRunning = false;
+
+ if (!m_studioSocketPtr.isNull()) {
+ m_studioSocketPtr->closeSocket();
+ m_studioSocketPtr->disconnect();
+ m_studioSocketPtr.reset();
+ }
+
+ // reset network statistics
+ m_networkStats = QJsonObject();
+
+ if (!m_currentStudio.id().isEmpty()) {
+ emit openFeedbackSurveyModal(m_currentStudio.id());
+ m_currentStudio.setId("");
+ emit currentStudioChanged();
+ }
+
+ m_interface.enableNap();
+}
+
+void VirtualStudio::editProfile()
+{
+ QUrl url = QUrl(QStringLiteral("https://www.jacktrip.com/profile"));
+ if (testMode()) {
+ url = QUrl(QStringLiteral("https://next-test.jacktrip.com/profile"));
+ }
+ QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::showAbout()
+{
+ emit openAboutWindow();
+}
+
+void VirtualStudio::openLink(const QString& link)
+{
+ QUrl url = QUrl(link);
+ QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::handleDeeplinkRequest(const QUrl& link)
+{
+ // check link is valid
+ QString studioId;
+ if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) {
+ qDebug() << "Ignoring invalid deeplink to" << link;
+ return;
+ }
+ if (link.host() == QLatin1String("join")) {
+ studioId = link.path().remove(0, 1);
+ } else if (link.host().isEmpty() && link.path().startsWith("join/")) {
+ studioId = link.path().remove(0, 5);
+ } else {
+ qDebug() << "Ignoring invalid deeplink to" << link;
+ return;
+ }
+
+ // check if already connected (ignore)
+ if (m_windowState == "connected" || m_windowState == "change_devices") {
+ qDebug() << "Already connected; ignoring deeplink to" << link;
+ return;
+ }
+
+ if (m_windowState == "setup"
+ && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) {
+ qDebug() << "Already preparing to connect; ignoring deeplink to" << link;
+ return;
+ }
+
+ qDebug() << "Handling deeplink to" << link;
+ setStudioToJoin(studioId);
+
+ // Switch to virtual studio mode, if necessary
+ // Note that this doesn't change the startup preference
+ if (m_interface.getMode() != UserInterface::MODE_VS) {
+ m_interface.setMode(UserInterface::MODE_VS);
+ } else {
+ raiseToTop();
+ }
+
+ // automatically navigate if on certain window screens
+ // note that the studio creation happens inside of the web view,
+ // and the app doesn't really know anything about it. we depend
+ // on the web app triggering a deep link join event, which is
+ // handled here. it's unlikely that the new studio has been
+ // noticed yet, so we don't join right away; otherwise we'd just
+ // get an unknown studio error. instead, we trigger a refresh and
+ // rely on it to kick off the join afterwards.
+ if (m_windowState == "browse" || m_windowState == "create_studio"
+ || m_windowState == "settings" || m_windowState == "setup"
+ || m_windowState == "failed") {
+ if (showDeviceSetup()) {
+ setWindowState("setup");
+ } else {
+ setWindowState("connected");
+ }
+ refreshStudios(0, true);
+ }
+
+ // otherwise, assume we are on setup screens and can let the normal flow handle it
+}
+
+void VirtualStudio::exit()
+{
+ // if multiple close events are received, emit the signalExit to force-quit the app
+ if (m_isExiting) {
+ emit signalExit();
+ }
+
+ // triggering isExitingChanged will force any WebEngine things to close properly
+ m_isExiting = true;
+ emit isExitingChanged();
+
+ // stop timers, clear data, etc.
+ resetState();
+
+ if (m_onConnectedScreen) {
+ // manually disconnect on self-managed studios
+ if (!m_currentStudio.id().isEmpty() && !m_currentStudio.isManaged()) {
+ disconnect();
+ }
+ } else {
+ emit signalExit();
+ }
+}
+
+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) {
+ m_apiHost = TEST_API_HOST;
+ }
+ m_api->setApiHost(m_apiHost);
+
+ // Get refresh token and userId
+ m_refreshToken = m_auth->refreshToken();
+ m_userId = m_auth->userId();
+ emit hasRefreshTokenChanged();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken);
+ settings.setValue(QStringLiteral("UserId"), m_userId);
+ settings.endGroup();
+
+ // initialize new VsDevice and wire up signals/slots before registering app
+ m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr));
+ connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this,
+ &VirtualStudio::updatedStats);
+ connect(m_audioConfigPtr.get(), &VsAudio::updatedInputVolume, m_devicePtr.get(),
+ &VsDevice::syncDeviceSettings);
+ connect(m_audioConfigPtr.get(), &VsAudio::updatedInputMuted, m_devicePtr.get(),
+ &VsDevice::syncDeviceSettings);
+ connect(m_audioConfigPtr.get(), &VsAudio::updatedOutputVolume, m_devicePtr.get(),
+ &VsDevice::syncDeviceSettings);
+ connect(m_audioConfigPtr.get(), &VsAudio::updatedMonitorVolume, m_devicePtr.get(),
+ &VsDevice::syncDeviceSettings);
+
+ m_devicePtr->registerApp();
+
+ if (!m_webChannelServer->listen(QHostAddress::LocalHost)) {
+ // shouldn't happen
+ std::cout << "ERROR: Failed to start server!" << std::endl;
+ }
+ m_webChannelPort = m_webChannelServer->serverPort();
+ emit webChannelPortChanged(m_webChannelPort);
+ std::cout << "QWebChannel listening on port: " << m_webChannelPort << std::endl;
+
+ getRegions();
+ getUserMetadata(); // calls refreshStudios(-1, false)
+}
+
+void VirtualStudio::connectionFinished()
+{
+ if (!m_devicePtr.isNull()
+ && (m_reconnectState == ReconnectState::RECONNECTING_VALIDATE
+ || m_reconnectState == ReconnectState::RECONNECTING_REFRESH)) {
+ if (m_devicePtr->hasTerminated()) {
+ if (m_reconnectState == ReconnectState::RECONNECTING_REFRESH) {
+ m_audioConfigPtr->refreshDevices(true);
+ } else {
+ m_audioConfigPtr->validateDevices(true);
+ }
+ connectToStudio();
+ }
+ return;
+ }
+ m_reconnectState = ReconnectState::NOT_RECONNECTING;
+
+ // use disconnect function to handle reset of all internal flags and timers
+ disconnect();
+}
+
+void VirtualStudio::processError(const QString& errorMessage)
+{
+ static const QString RtAudioErrorMsg = QStringLiteral("RtAudio Error");
+ static const QString JackAudioErrorMsg =
+ QStringLiteral("The Jack server was shut down");
+
+ const bool shouldSwitchToRtAudio =
+ (errorMessage == QLatin1String("Maybe the JACK server is not running?"));
+
+ QMessageBox msgBox;
+ if (shouldSwitchToRtAudio) {
+ // Report the other end quitting as a regular occurance rather than an error.
+ msgBox.setText("The JACK server is not running. Switching back to RtAudio.");
+ msgBox.setWindowTitle(QStringLiteral("No JACK server"));
+ } else if (errorMessage == QLatin1String("Peer Stopped")) {
+ // Report the other end quitting as a regular occurance rather than an error.
+ msgBox.setText("The Studio has been stopped.");
+ msgBox.setWindowTitle(QStringLiteral("Disconnected"));
+ } else if (errorMessage.startsWith(RtAudioErrorMsg)) {
+ if (errorMessage.length() > RtAudioErrorMsg.length() + 2) {
+ const QString details(errorMessage.sliced(RtAudioErrorMsg.length() + 2));
+ if (details.contains(QStringLiteral("device was disconnected"))
+ || details.contains(
+ QStringLiteral("Unable to retrieve capture buffer"))) {
+ msgBox.setText(QStringLiteral("Your audio interface was disconnected."));
+ } else {
+ msgBox.setText(details);
+ }
+ } else {
+ msgBox.setText(errorMessage);
+ }
+ msgBox.setWindowTitle(QStringLiteral("Audio Interface Error"));
+ } else if (errorMessage.startsWith(JackAudioErrorMsg)) {
+ if (errorMessage.length() > JackAudioErrorMsg.length() + 2) {
+ msgBox.setText(errorMessage.sliced(JackAudioErrorMsg.length() + 2));
+ } else {
+ msgBox.setText(QStringLiteral("The JACK Audio Server was stopped."));
+ }
+ msgBox.setWindowTitle(QStringLiteral("Jack Audio Error"));
+ } else {
+ msgBox.setText(QStringLiteral("Error: ").append(errorMessage));
+ msgBox.setWindowTitle(QStringLiteral("Doh!"));
+ }
+ msgBox.exec();
+
+ if (m_jackTripRunning)
+ connectionFinished();
+}
+
+void VirtualStudio::receivedConnectionFromPeer()
+{
+ // Connect via API
+ m_connectionState = QStringLiteral("Connected");
+ emit connectionStateChanged();
+ std::cout << "Received connection" << std::endl;
+ emit connected();
+}
+
+void VirtualStudio::handleWebsocketMessage(const QString& msg)
+{
+ QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object();
+ QString serverStatus = serverState[QStringLiteral("status")].toString();
+ bool serverEnabled = serverState[QStringLiteral("enabled")].toBool();
+ QString serverCloudId = serverState[QStringLiteral("cloudId")].toString();
+
+ // server notifications are also transmitted along this websocket, so ignore data if
+ // it contains "message"
+ QString message = serverState[QStringLiteral("message")].toString();
+ if (!message.isEmpty()) {
+ return;
+ }
+ if (m_currentStudio.id() == "") {
+ return;
+ }
+ m_currentStudio.setStatus(serverStatus);
+ m_currentStudio.setEnabled(serverEnabled);
+ m_currentStudio.setCloudId(serverCloudId);
+ if (!m_jackTripRunning) {
+ if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) {
+ m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString());
+ m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt());
+ m_currentStudio.setSessionId(
+ serverState[QStringLiteral("sessionId")].toString());
+ completeConnection();
+ }
+ }
+
+ emit currentStudioChanged();
+}
+
+void VirtualStudio::restartStudioSocket()
+{
+ if (m_onConnectedScreen) {
+ if (!m_studioSocketPtr.isNull()) {
+ m_studioSocketPtr->openSocket();
+ }
+ }
+}
+
+void VirtualStudio::updatedStats(const QJsonObject& stats)
+{
+ QJsonObject newStats;
+ for (int i = 0; i < stats.keys().size(); i++) {
+ QString key = stats.keys().at(i);
+ newStats.insert(key, stats[key].toDouble());
+ }
+
+ m_networkStats = newStats;
+ emit networkStatsChanged();
+ return;
+}
+
+void VirtualStudio::udpWaitingTooLong()
+{
+ if (m_devicePtr.isNull())
+ return;
+ m_networkOutageTimer.start();
+ m_devicePtr->setNetworkOutage(true);
+ emit updatedNetworkOutage(true);
+}
+
+void VirtualStudio::sendHeartbeat()
+{
+ if (!m_devicePtr.isNull() && m_connectionState != "Connecting..."
+ && m_connectionState != "Preparing audio...") {
+ m_devicePtr->sendHeartbeat();
+ }
+}
+
+void VirtualStudio::resetState()
+{
+ m_webChannelServer->close();
+ m_heartbeatTimer.stop();
+ m_startTimer.stop();
+ m_networkOutageTimer.stop();
+ m_firstRefresh = true;
+}
+
+void VirtualStudio::refreshStudios(int index, bool signalRefresh)
+{
+ // user id is required for retrieval of subscriptions
+ if (m_userId.isEmpty()) {
+ std::cerr << "Studio refresh cancelled due to empty user id" << std::endl;
+ return;
+ }
+
+ // only allow one thread to refresh at a time
+ QMutexLocker refreshLock(&m_refreshMutex);
+ if (m_refreshInProgress)
+ return;
+ std::cout << "Refreshing list of studios" << std::endl;
+ setRefreshInProgress(true);
+ refreshLock.unlock();
+
+ // get subscriptions first; this is required for studio groupings
+ QNetworkReply* reply = m_api->getSubscriptions(m_userId);
+ connect(reply, &QNetworkReply::finished, this, [&, reply, signalRefresh, index]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ }
+
+ QByteArray response = reply->readAll();
+ QJsonDocument subscriptionList = QJsonDocument::fromJson(response);
+ if (!subscriptionList.isArray()) {
+ std::cout << "Error: Not an array" << std::endl;
+ reply->deleteLater();
+ return;
+ }
+ m_subscribedServers.clear();
+ QJsonArray subscriptions = subscriptionList.array();
+ for (int i = 0; i < subscriptions.count(); i++) {
+ m_subscribedServers.insert(
+ subscriptions.at(i)[QStringLiteral("serverId")].toString(), true);
+ }
+ reply->deleteLater();
+
+ // done getting subscriptions; get list of servers next
+ QNetworkReply* serversReply = m_api->getServers();
+ connect(serversReply, &QNetworkReply::finished, this,
+ [&, serversReply, signalRefresh, index]() {
+ handleServerUpdate(serversReply, signalRefresh, index);
+ });
+ });
+}
+
+void VirtualStudio::handleServerUpdate(QNetworkReply* reply, bool signalRefresh,
+ int index)
+{
+ if (reply->error() != QNetworkReply::NoError) {
+ if (signalRefresh) {
+ emit refreshFinished(index);
+ }
+ std::cerr << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ QMutexLocker refreshLock(&m_refreshMutex);
+ setRefreshInProgress(false);
+ return;
+ }
+
+ // Get the serverId of the server at the top of our screen if we know it
+ QString topServerId;
+ if (index >= 0 && index < m_serverModel.count()) {
+ topServerId = m_serverModel.at(index)->id();
+ }
+
+ QByteArray response = reply->readAll();
+ QJsonDocument serverList = QJsonDocument::fromJson(response);
+ reply->deleteLater();
+ if (!serverList.isArray()) {
+ if (signalRefresh) {
+ emit refreshFinished(index);
+ }
+ std::cerr << "Error: Not an array" << std::endl;
+ QMutexLocker refreshLock(&m_refreshMutex);
+ setRefreshInProgress(false);
+ return;
+ }
+
+ QJsonArray servers = serverList.array();
+ // Divide our servers by category initially so that they're easier to sort
+ QVector<VsServerInfoPointer> yourServers;
+ QVector<VsServerInfoPointer> subServers;
+ QVector<VsServerInfoPointer> pubServers;
+ int skippedStudios = 0;
+
+ QMutexLocker refreshLock(&m_refreshMutex); // protect m_servers
+ m_servers.clear();
+ for (int i = 0; i < servers.count(); i++) {
+ if (servers.at(i)[QStringLiteral("type")].toString().contains(
+ QStringLiteral("JackTrip"))) {
+ QSharedPointer<VsServerInfo> serverInfo(new VsServerInfo(this));
+ serverInfo->setIsAdmin(servers.at(i)[QStringLiteral("admin")].toBool());
+ serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString());
+ serverInfo->setHost(servers.at(i)[QStringLiteral("serverHost")].toString());
+ serverInfo->setIsManaged(servers.at(i)[QStringLiteral("managed")].toBool());
+ 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->setStreamId(servers.at(i)[QStringLiteral("streamId")].toString());
+ serverInfo->setInviteKey(
+ servers.at(i)[QStringLiteral("inviteKey")].toString());
+ serverInfo->setCloudId(servers.at(i)[QStringLiteral("cloudId")].toString());
+ serverInfo->setEnabled(servers.at(i)[QStringLiteral("enabled")].toBool());
+ serverInfo->setIsOwner(servers.at(i)[QStringLiteral("owner")].toBool());
+
+ // Always add servers to m_servers
+ m_servers.append(serverInfo);
+
+ // Only add servers to the model that we want to show
+ if (serverInfo->isAdmin() || serverInfo->isOwner()) {
+ if (filterStudio(*serverInfo)) {
+ ++skippedStudios;
+ } else {
+ yourServers.append(serverInfo);
+ serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
+ }
+ } else if (m_subscribedServers.contains(serverInfo->id())) {
+ if (filterStudio(*serverInfo)) {
+ ++skippedStudios;
+ } else {
+ subServers.append(serverInfo);
+ serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
+ }
+ } else {
+ if (!filterStudio(*serverInfo) && serverInfo->enabled()) {
+ pubServers.append(serverInfo);
+ serverInfo->setSection(VsServerInfo::PUBLIC_STUDIOS);
+ }
+ // don't count public studios in skipped count
+ }
+ }
+ }
+ refreshLock.unlock();
+
+ // sort studios in each section by name
+ auto serverSorter = [](VsServerInfoPointer first, VsServerInfoPointer second) {
+ return *first < *second;
+ };
+ std::sort(yourServers.begin(), yourServers.end(), serverSorter);
+ std::sort(subServers.begin(), subServers.end(), serverSorter);
+ std::sort(pubServers.begin(), pubServers.end(), serverSorter);
+
+ // 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 (skippedStudios == 0 && m_windowState == "browse") {
+ // Transition to create studio page
+ setWindowState("create_studio");
+ }
+ } else {
+ m_logoSection = QStringLiteral("Subscribed Studios");
+ }
+ } else {
+ m_logoSection = QStringLiteral("Your Studios");
+ }
+ emit logoSectionChanged();
+
+ m_serverModel.clear();
+ for (const VsServerInfoPointer& s : yourServers) {
+ m_serverModel.append(s.get());
+ }
+ for (const VsServerInfoPointer& s : subServers) {
+ m_serverModel.append(s.get());
+ }
+ for (const VsServerInfoPointer& s : pubServers) {
+ m_serverModel.append(s.get());
+ }
+ emit serverModelChanged();
+
+ index = -1;
+ if (!topServerId.isEmpty()) {
+ for (int i = 0; i < m_serverModel.count(); i++) {
+ if (m_serverModel.at(i)->id() == topServerId) {
+ index = i;
+ break;
+ }
+ }
+ }
+
+ if (m_firstRefresh) {
+ m_heartbeatTimer.setInterval(5000);
+ m_heartbeatTimer.start();
+ m_firstRefresh = false;
+ }
+ setRefreshInProgress(false);
+ if (signalRefresh) {
+ emit refreshFinished(index);
+ }
+}
+
+bool VirtualStudio::filterStudio(const VsServerInfo& serverInfo) const
+{
+ // Return true if we want to filter the studio out of the display model
+ bool activeStudio = serverInfo.status() == QLatin1String("Ready");
+ bool hostedStudio = serverInfo.isManaged();
+ if (!m_showSelfHosted && !hostedStudio) {
+ return true;
+ }
+ if (!m_showInactive && !activeStudio) {
+ return true;
+ }
+ return false;
+}
+
+void VirtualStudio::getRegions()
+{
+ QNetworkReply* reply = m_api->getRegions(m_userId);
+ connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ }
+
+ m_regions = QJsonDocument::fromJson(reply->readAll()).object();
+ emit regionsChanged();
+ reply->deleteLater();
+ });
+}
+
+void VirtualStudio::getUserMetadata()
+{
+ QNetworkReply* reply = m_api->getUser(m_userId);
+ connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ }
+
+ m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object();
+ emit userMetadataChanged();
+ reply->deleteLater();
+ refreshStudios(-1, false);
+ });
+}
+
+bool VirtualStudio::readyToJoin()
+{
+ // FTUX shows warnings and device setup views
+ // if any of these enabled, do not immediately join
+ return m_windowState == "connected"
+ && (m_connectionState == QStringLiteral("Waiting...")
+ || m_connectionState == QStringLiteral("Disconnected"));
+}
+
+void VirtualStudio::detectedFeedbackLoop()
+{
+ emit feedbackDetected();
+}
+
+VirtualStudio::~VirtualStudio()
+{
+ QDesktopServices::unsetUrlHandler("jacktrip");
+ // close the window
+ m_view.reset();
+ // stop the audio worker thread before destructing other things
+ m_audioConfigPtr.reset();
+ // stop device and corresponding threads
+ m_devicePtr.reset();
+}
+
+QApplication* VirtualStudio::createApplication(int& argc, char* argv[])
+{
+#if defined(Q_OS_WIN)
+ // Fix for display scaling like 125% or 150% on Windows
+ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+
+ // 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);
+#endif
+
+ QQuickStyle::setStyle("Basic");
+
+ // Initialize webengine
+ QtWebEngineQuick::initialize();
+ // TODO: Add support for QtWebView
+ // qputenv("QT_WEBVIEW_PLUGIN", "native");
+ // QtWebView::initialize();
+
+#if defined(Q_OS_MACOS)
+ return new JTApplication(argc, argv);
+#else
+ return new QApplication(argc, argv);
+#endif
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 virtualstudio.h
+ * \author Matt Horton, based on code by Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VIRTUALSTUDIO_H
+#define VIRTUALSTUDIO_H
+
+#include <QJsonObject>
+#include <QMap>
+#include <QMutex>
+#include <QNetworkAccessManager>
+#include <QObject>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QString>
+#include <QStringList>
+#include <QTimer>
+#include <QUrl>
+#include <QVector>
+#include <QWebChannel>
+#include <QWebSocketServer>
+
+#include "../Settings.h"
+#include "../UserInterface.h"
+#include "vsConstants.h"
+#include "vsQuickView.h"
+#include "vsServerInfo.h"
+
+class JackTrip;
+class VsAudio;
+class VsApi;
+class VsAuth;
+class VsDevice;
+class VsDeeplink;
+class VsWebSocket;
+
+typedef QSharedPointer<VsServerInfo> VsServerInfoPointer;
+
+class VirtualStudio : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged)
+ Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged)
+ Q_PROPERTY(QString versionString READ versionString CONSTANT)
+ Q_PROPERTY(QString buildString READ buildString CONSTANT)
+ Q_PROPERTY(QString copyrightString READ copyrightString CONSTANT)
+ Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged)
+ Q_PROPERTY(
+ QString connectedErrorMsg READ connectedErrorMsg NOTIFY connectedErrorMsgChanged)
+
+ Q_PROPERTY(
+ QVector<VsServerInfo*> serverModel READ getServerModel NOTIFY serverModelChanged)
+ Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged)
+ Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY
+ studioToJoinChanged)
+ Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged)
+ Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged)
+ Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY
+ showInactiveChanged)
+ Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY
+ showSelfHostedChanged)
+ Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged)
+ Q_PROPERTY(QJsonObject networkStats READ networkStats NOTIFY networkStatsChanged)
+ Q_PROPERTY(bool networkOutage READ networkOutage NOTIFY updatedNetworkOutage)
+
+ Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
+ updateChannelChanged)
+ Q_PROPERTY(float fontScale READ fontScale CONSTANT)
+ Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged)
+ Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
+ Q_PROPERTY(bool collapseDeviceControls READ collapseDeviceControls WRITE
+ setCollapseDeviceControls NOTIFY collapseDeviceControlsChanged)
+ Q_PROPERTY(bool testMode READ testMode WRITE setTestMode NOTIFY testModeChanged)
+ Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY
+ showDeviceSetupChanged)
+ Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY
+ showWarningsChanged)
+ Q_PROPERTY(bool isExiting READ isExiting NOTIFY isExitingChanged)
+ Q_PROPERTY(
+ bool refreshInProgress READ refreshInProgress NOTIFY refreshInProgressChanged)
+ Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT)
+ Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT)
+ Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged)
+ Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY
+ windowStateUpdated)
+ Q_PROPERTY(QString apiHost READ apiHost WRITE setApiHost NOTIFY apiHostChanged)
+ Q_PROPERTY(bool vsFtux READ vsFtux CONSTANT)
+ Q_PROPERTY(bool hasClassicMode READ hasClassicMode CONSTANT)
+ Q_PROPERTY(
+ QStringList updateChannelComboModel READ getUpdateChannelComboModel CONSTANT)
+
+ public:
+ explicit VirtualStudio(UserInterface& parent);
+ ~VirtualStudio() override;
+
+ void show();
+ void hide();
+ void raiseToTop();
+
+ int webChannelPort();
+ bool hasRefreshToken();
+ QString versionString();
+ QString buildString();
+ QString copyrightString();
+ QString logoSection();
+ QString connectedErrorMsg();
+ void setConnectedErrorMsg(const QString& msg);
+ const QVector<VsServerInfo*>& getServerModel() const { return m_serverModel; }
+ VsServerInfo* currentStudio() { return &m_currentStudio; }
+ QJsonObject regions();
+ QJsonObject userMetadata();
+ QString connectionState();
+ QJsonObject networkStats();
+ QString updateChannel();
+ void setUpdateChannel(const QString& channel);
+ bool showInactive();
+ void setShowInactive(bool inactive);
+ bool showSelfHosted();
+ void setShowSelfHosted(bool selfHosted);
+ float fontScale();
+ float uiScale();
+ void setUiScale(float scale);
+ bool darkMode();
+ void setDarkMode(bool dark);
+ bool collapseDeviceControls();
+ void setCollapseDeviceControls(bool collapseDeviceControls);
+ bool testMode();
+ void setTestMode(bool test);
+ QString studioToJoin();
+ void setStudioToJoin(const QString& id);
+ bool showDeviceSetup();
+ void setShowDeviceSetup(bool show);
+ bool showWarnings();
+ void setShowWarnings(bool show);
+ bool noUpdater();
+ bool psiBuild();
+ QString failedMessage();
+ bool networkOutage();
+ bool backendAvailable();
+ QString windowState();
+ QString apiHost();
+ void setApiHost(QString host);
+ bool vsFtux();
+ bool hasClassicMode();
+ bool isExiting();
+ bool refreshInProgress();
+ void setRefreshInProgress(bool b);
+
+ static QApplication* createApplication(int& argc, char* argv[]);
+
+ const QStringList& getUpdateChannelComboModel() const
+ {
+ return m_updateChannelOptions;
+ }
+
+ public slots:
+ void toVirtualStudioMode();
+ void toClassicMode();
+ void login();
+ void logout();
+ void refreshStudios(int index, bool signalRefresh = false);
+ void loadSettings();
+ void saveSettings();
+ void triggerReconnect(bool refresh);
+ void editProfile();
+ void showAbout();
+ void openLink(const QString& url);
+ void handleDeeplinkRequest(const QUrl& url);
+ void udpWaitingTooLong();
+ void setWindowState(QString state);
+ void joinStudio();
+ void disconnect();
+ void collectFeedbackSurvey(QString serverId, int rating, QString message);
+
+ signals:
+ void failed();
+ void connected();
+ void disconnected();
+ void refreshFinished(int index);
+ void webChannelPortChanged(int webChannelPort);
+ void hasRefreshTokenChanged();
+ void logoSectionChanged();
+ void connectedErrorMsgChanged();
+ void serverModelChanged();
+ void currentStudioChanged();
+ void regionsChanged();
+ void userMetadataChanged();
+ void showInactiveChanged();
+ void showSelfHostedChanged();
+ void connectionStateChanged();
+ void networkStatsChanged();
+ void updateChannelChanged();
+ void showDeviceSetupChanged();
+ void showWarningsChanged();
+ void uiScaleChanged();
+ void collapseDeviceControlsChanged(bool collapseDeviceControls);
+ void newScale();
+ void darkModeChanged();
+ void testModeChanged();
+ void signalExit();
+ void failedMessageChanged();
+ void studioToJoinChanged();
+ void updatedNetworkOutage(bool outage);
+ void windowStateUpdated();
+ void isExitingChanged();
+ void scheduleStudioRefresh(int index, bool signalRefresh);
+ void refreshInProgressChanged();
+ void apiHostChanged();
+ void feedbackDetected();
+ void openFeedbackSurveyModal(QString serverId);
+ void openAboutWindow();
+
+ private slots:
+ void slotAuthSucceeded();
+ void receivedConnectionFromPeer();
+ void handleWebsocketMessage(const QString& msg);
+ void restartStudioSocket();
+ void updatedStats(const QJsonObject& stats);
+ void processError(const QString& errorMessage);
+ void detectedFeedbackLoop();
+ void sendHeartbeat();
+ void connectionFinished();
+ void exit();
+
+ private:
+ void resetState();
+ void handleServerUpdate(QNetworkReply* reply, bool signalRefresh, int index);
+ bool filterStudio(const VsServerInfo& serverInfo) const;
+ void getRegions();
+ void getUserMetadata();
+ bool readyToJoin();
+ void connectToStudio();
+ void completeConnection();
+
+ private:
+ enum ReconnectState {
+ NOT_RECONNECTING = 0,
+ RECONNECTING_VALIDATE,
+ RECONNECTING_REFRESH
+ };
+
+ UserInterface& m_interface;
+ VsServerInfo m_currentStudio;
+ QNetworkAccessManager* m_networkAccessManagerPtr;
+ QScopedPointer<VsQuickView> m_view;
+ QSharedPointer<VsDeeplink> m_deepLinkPtr;
+ QSharedPointer<VsAuth> m_auth;
+ QSharedPointer<VsApi> m_api;
+ QScopedPointer<VsDevice> m_devicePtr;
+ QScopedPointer<VsWebSocket> m_studioSocketPtr;
+ QSharedPointer<VsAudio> m_audioConfigPtr;
+ QVector<VsServerInfoPointer> m_servers;
+ QVector<VsServerInfo*> m_serverModel; //< qml doesn't like smart pointers
+ QScopedPointer<QWebSocketServer> m_webChannelServer;
+ QScopedPointer<QWebChannel> m_webChannel;
+ QMap<QString, bool> m_subscribedServers;
+ QJsonObject m_regions;
+ QJsonObject m_userMetadata;
+ QJsonObject m_networkStats;
+ QTimer m_startTimer;
+ QTimer m_heartbeatTimer;
+ QTimer m_networkOutageTimer;
+ QMutex m_refreshMutex;
+ QString m_studioToJoin;
+ QString m_updateChannel;
+ QString m_refreshToken;
+ QString m_userId;
+ QString m_apiHost = PROD_API_HOST;
+ ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING;
+
+ bool m_firstRefresh = true;
+ bool m_jackTripRunning = false;
+ bool m_checkSsl = true;
+ bool m_refreshInProgress = false;
+ bool m_onConnectedScreen = false;
+ bool m_isExiting = false;
+ bool m_showInactive = true;
+ bool m_showSelfHosted = true;
+ bool m_showDeviceSetup = true;
+ bool m_showWarnings = true;
+ bool m_darkMode = false;
+ bool m_collapseDeviceControls = false;
+ bool m_testMode = false;
+ bool m_authenticated = false;
+ float m_fontScale = 1;
+ float m_uiScale = 1;
+ uint32_t m_webChannelPort = 1;
+
+ QString m_failedMessage = QStringLiteral("");
+ QString m_windowState = QStringLiteral("");
+ QString m_connectedErrorMsg = QStringLiteral("");
+ QString m_logoSection = QStringLiteral("Your Studios");
+ QString m_connectionState = QStringLiteral("Waiting...");
+ QStringList m_updateChannelOptions = {"Stable", "Edge"};
+
+#if defined(VS_FTUX) || defined(NO_CLASSIC)
+ bool m_vsFtux = true;
+#else
+ bool m_vsFtux = false;
+#endif
+};
+
+#endif // VIRTUALSTUDIO_H
--- /dev/null
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+ property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+
+ color: backgroundColour
+ state: virtualstudio.windowState
+ anchors.fill: parent
+
+ id: window
+ states: [
+ State {
+ name: "start"
+ PropertyChanges { target: startScreen; x: 0 }
+ PropertyChanges { target: loginScreen; x: window.width; }
+ PropertyChanges { target: recommendationsScreen; x: window.width }
+ PropertyChanges { target: permissionsScreen; x: window.width }
+ 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 }
+ },
+
+ State {
+ name: "login"
+ PropertyChanges { target: startScreen; x: -startScreen.width }
+ PropertyChanges { target: loginScreen; x: 0; }
+ PropertyChanges { target: recommendationsScreen; x: window.width }
+ PropertyChanges { target: permissionsScreen; x: window.width }
+ 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 }
+ },
+
+ State {
+ name: "recommendations"
+ PropertyChanges { target: loginScreen; x: -loginScreen.width }
+ PropertyChanges { target: startScreen; x: -startScreen.width }
+ PropertyChanges { target: recommendationsScreen; x: 0 }
+ PropertyChanges { target: permissionsScreen; x: window.width }
+ 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 }
+ },
+
+ State {
+ name: "permissions"
+ PropertyChanges { target: loginScreen; x: -loginScreen.width }
+ PropertyChanges { target: startScreen; x: -startScreen.width }
+ PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+ PropertyChanges { target: permissionsScreen; x: 0 }
+ 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 }
+ },
+
+ State {
+ name: "setup"
+ 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: 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 }
+ },
+
+ State {
+ name: "browse"
+ 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: 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 }
+ },
+
+ State {
+ name: "settings"
+ 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: 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 }
+ PropertyChanges { target: startScreen; x: -startScreen.width }
+ PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+ PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+ 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 }
+ },
+
+ State {
+ name: "change_devices"
+ 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: 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 }
+ },
+
+ State {
+ name: "failed"
+ 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: -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 }
+ }
+ ]
+
+ transitions: Transition {
+ NumberAnimation { properties: "x"; duration: 500; easing.type: Easing.InOutQuad }
+ }
+
+ FirstLaunch {
+ id: startScreen
+ x: -startScreen.width
+ }
+
+ Login {
+ id: loginScreen
+ x: window.width
+ }
+
+ Recommendations {
+ id: recommendationsScreen
+ x: window.width
+ }
+
+ Permissions {
+ id: permissionsScreen
+ x: window.width
+ }
+
+ Browse {
+ id: browseScreen
+ x: window.width
+ }
+
+ Setup {
+ id: setupScreen
+ x: window.width
+ }
+
+ Settings {
+ id: settingsScreen
+ x: window.width
+ }
+
+ Connected {
+ id: connectedScreen
+ x: window.width
+ }
+
+ ChangeDevices {
+ id: changeDevicesScreen
+ x: window.width
+ }
+
+ CreateStudio {
+ id: createStudioScreen
+ x: window.width
+ }
+
+ Failed {
+ id: failedScreen
+ x: window.width
+ }
+
+ onWidthChanged: {
+ if (virtualstudio.windowState === "start") {
+ startScreen.x = 0
+ } else if (virtualstudio.windowState === "login") {
+ loginScreen.x = 0
+ } else if (virtualstudio.windowState === "recommendations") {
+ recommendationsScreen.x = 0;
+ } else if (virtualstudio.windowState === "permissions") {
+ permissionsScreen.x = 0;
+ } else if (virtualstudio.windowState === "setup") {
+ setupScreen.x = 0
+ } else if (virtualstudio.windowState === "browse") {
+ 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") {
+ changeDevicesScreen.x = 0
+ } else if (virtualstudio.windowState === "failed") {
+ failedScreen.x = 0
+ }
+ }
+
+ onHeightChanged: {
+ if (virtualstudio.windowState === "start") {
+ startScreen.x = 0
+ } else if (virtualstudio.windowState === "login") {
+ loginScreen.x = 0
+ } else if (virtualstudio.windowState === "recommendations") {
+ recommendationsScreen.x = 0;
+ } else if (virtualstudio.windowState === "permissions") {
+ permissionsScreen.x = 0;
+ } else if (virtualstudio.windowState === "setup") {
+ setupScreen.x = 0
+ } else if (virtualstudio.windowState === "browse") {
+ 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") {
+ changeDevicesScreen.x = 0
+ } else if (virtualstudio.windowState === "failed") {
+ failedScreen.x = 0
+ }
+ }
+
+ Connections {
+ target: auth
+ function onAuthSucceeded() {
+ if (virtualstudio.windowState !== "login") {
+ // can happen on settings screen when switching between prod and test
+ return;
+ }
+ if (virtualstudio.showWarnings) {
+ virtualstudio.windowState = "recommendations";
+ } else if (virtualstudio.studioToJoin === "") {
+ virtualstudio.windowState = "browse";
+ } else {
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
+ }
+ }
+ }
+ Connections {
+ target: virtualstudio
+ function onConnected() {
+ if (virtualstudio.windowState == "change_devices") {
+ return;
+ }
+ virtualstudio.windowState = "connected";
+ }
+ function onFailed() {
+ virtualstudio.windowState = "failed";
+ }
+ function onDisconnected() {
+ virtualstudio.windowState = "browse";
+ }
+ }
+}
--- /dev/null
+<RCC>
+ <qresource prefix="vs">
+ <file>vs.qml</file>
+ <file>AboutWindow.qml</file>
+ <file>FirstLaunch.qml</file>
+ <file>Login.qml</file>
+ <file>LearnMoreButton.qml</file>
+ <file>Recommendations.qml</file>
+ <file>Permissions.qml</file>
+ <file>ChangeDevices.qml</file>
+ <file>Studio.qml</file>
+ <file>Browse.qml</file>
+ <file>AudioSettings.qml</file>
+ <file>Settings.qml</file>
+ <file>Meter.qml</file>
+ <file>MeterBars.qml</file>
+ <file>Connected.qml</file>
+ <file>CreateStudio.qml</file>
+ <file>Failed.qml</file>
+ <file>Setup.qml</file>
+ <file>SectionHeading.qml</file>
+ <file>Footer.qml</file>
+ <file>VolumeSlider.qml</file>
+ <file>DeviceControls.qml</file>
+ <file>DeviceControlsGroup.qml</file>
+ <file>DeviceRefreshButton.qml</file>
+ <file>DeviceWarning.qml</file>
+ <file>DeviceWarningModal.qml</file>
+ <file>InfoTooltip.qml</file>
+ <file>Web.qml</file>
+ <file>WebView.qml</file>
+ <file>WebEngine.qml</file>
+ <file>WebNull.qml</file>
+ <file>FeedbackSurvey.qml</file>
+ <file>AppIcon.qml</file>
+ <file>logo.svg</file>
+ <file>wedge.svg</file>
+ <file>wedge_inactive.svg</file>
+ <file>private.svg</file>
+ <file>public.svg</file>
+ <file>join.svg</file>
+ <file>leave.svg</file>
+ <file>manage.svg</file>
+ <file>speed.svg</file>
+ <file>share.svg</file>
+ <file>start.svg</file>
+ <file>star.svg</file>
+ <file>cog.svg</file>
+ <file>mic.svg</file>
+ <file>language.svg</file>
+ <file>micoff.svg</file>
+ <file>help.svg</file>
+ <file>quiet.svg</file>
+ <file>loud.svg</file>
+ <file>refresh.svg</file>
+ <file>ethernet.svg</file>
+ <file>networkCheck.svg</file>
+ <file>externalMic.svg</file>
+ <file>check.svg</file>
+ <file>warning.svg</file>
+ <file>expand_less.svg</file>
+ <file>expand_more.svg</file>
+ <file>sentiment_very_dissatisfied.svg</file>
+ <file>headphones.svg</file>
+ <file>Prompt.svg</file>
+ <file>network.svg</file>
+ <file>video.svg</file>
+ <file>close.svg</file>
+ <file>jacktrip.png</file>
+ <file>jacktrip white.png</file>
+ <file>JTOriginal.png</file>
+ <file>JTVS.png</file>
+ <file>flags/AE.svg</file>
+ <file>flags/AU.svg</file>
+ <file>flags/BE.svg</file>
+ <file>flags/BR.svg</file>
+ <file>flags/CA.svg</file>
+ <file>flags/CH.svg</file>
+ <file>flags/DE.svg</file>
+ <file>flags/FR.svg</file>
+ <file>flags/GB.svg</file>
+ <file>flags/HK.svg</file>
+ <file>flags/ID.svg</file>
+ <file>flags/IT.svg</file>
+ <file>flags/JP.svg</file>
+ <file>flags/RO.svg</file>
+ <file>flags/SE.svg</file>
+ <file>flags/SG.svg</file>
+ <file>flags/TW.svg</file>
+ <file>flags/US.svg</file>
+ <file>flags/ZA.svg</file>
+ <file>Poppins-Bold.ttf</file>
+ <file>Poppins-Regular.ttf</file>
+ </qresource>
+</RCC>
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsApi.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "vsApi.h"
+
+#include "../jacktrip_globals.h"
+
+VsApi::VsApi(QNetworkAccessManager* networkAccessManager)
+{
+ m_networkAccessManager = networkAccessManager;
+}
+
+QNetworkReply* VsApi::getAuth0UserInfo()
+{
+ return get(QUrl("https://auth.jacktrip.org/userinfo"));
+}
+
+QNetworkReply* VsApi::getUser(const QString& userId)
+{
+ return get(QUrl(QString("https://%1/api/users/%2").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getServers()
+{
+ return get(QUrl(QString("https://%1/api/servers").arg(m_apiHost)));
+}
+
+QNetworkReply* VsApi::getSubscriptions(const QString& userId)
+{
+ return get(
+ QUrl(QString("https://%1/api/users/%2/subscriptions").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getRegions(const QString& userId)
+{
+ return get(QUrl(QString("https://%1/api/users/%2/regions").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getDevice(const QString& deviceId)
+{
+ return get(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
+}
+
+QNetworkReply* VsApi::postDevice(const QByteArray& data)
+{
+ return post(QUrl(QString("https://%1/api/devices").arg(m_apiHost)), data);
+}
+
+QNetworkReply* VsApi::postDeviceHeartbeat(const QString& deviceId, const QByteArray& data)
+{
+ return post(
+ QUrl(QString("https://%1/api/devices/%2/heartbeat").arg(m_apiHost, deviceId)),
+ data);
+}
+
+QNetworkReply* VsApi::submitServerFeedback(const QString& serverId,
+ const QByteArray& data)
+{
+ return post(
+ QUrl(QString("https://%1/api/servers/%2/feedback").arg(m_apiHost, serverId)),
+ data);
+}
+
+QNetworkReply* VsApi::updateServer(const QString& serverId, const QByteArray& data)
+{
+ return put(QUrl(QString("https://%1/api/servers/%2").arg(m_apiHost, serverId)), data);
+}
+
+QNetworkReply* VsApi::updateDevice(const QString& deviceId, const QByteArray& data)
+{
+ return put(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)), data);
+}
+
+QNetworkReply* VsApi::deleteDevice(const QString& deviceId)
+{
+ return deleteResource(
+ QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
+}
+
+QNetworkReply* VsApi::get(const QUrl& url)
+{
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setRawHeader(QByteArray("User-Agent"),
+ QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+ request.setRawHeader(QByteArray("Authorization"),
+ QString("Bearer %1").arg(m_accessToken).toUtf8());
+
+ QNetworkReply* reply = m_networkAccessManager->get(request);
+ return reply;
+}
+
+QNetworkReply* VsApi::post(const QUrl& url, const QByteArray& data)
+{
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setRawHeader(QByteArray("User-Agent"),
+ QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+ request.setRawHeader(QByteArray("Authorization"),
+ QString("Bearer %1").arg(m_accessToken).toUtf8());
+ request.setRawHeader(QByteArray("Content-Type"),
+ QString("application/json").toUtf8());
+
+ QNetworkReply* reply = m_networkAccessManager->post(request, data);
+ return reply;
+}
+
+QNetworkReply* VsApi::put(const QUrl& url, const QByteArray& data)
+{
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setRawHeader(QByteArray("User-Agent"),
+ QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+ request.setRawHeader(QByteArray("Authorization"),
+ QString("Bearer %1").arg(m_accessToken).toUtf8());
+ request.setRawHeader(QByteArray("Content-Type"),
+ QString("application/json").toUtf8());
+ QNetworkReply* reply = m_networkAccessManager->put(request, data);
+ return reply;
+}
+
+QNetworkReply* VsApi::deleteResource(const QUrl& url)
+{
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setRawHeader(QByteArray("User-Agent"),
+ QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+ request.setRawHeader(QByteArray("Authorization"),
+ QString("Bearer %1").arg(m_accessToken).toUtf8());
+
+ QNetworkReply* reply = m_networkAccessManager->deleteResource(request);
+ return reply;
+}
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsApi.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSAPI_H
+#define VSAPI_H
+
+#include <QEventLoop>
+#include <QJsonParseError>
+#include <QMap>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QString>
+#include <QUrl>
+#include <QVariant>
+#include <iostream>
+
+class VsApi : public QObject
+{
+ Q_OBJECT
+
+ public:
+ VsApi(QNetworkAccessManager* networkAccessManager);
+ void setAccessToken(QString token) { m_accessToken = token; };
+ void setApiHost(QString host) { m_apiHost = host; }
+ QString getApiHost() { return m_apiHost; }
+
+ QNetworkReply* getAuth0UserInfo();
+ QNetworkReply* getUser(const QString& userId);
+ QNetworkReply* getServers();
+ QNetworkReply* getSubscriptions(const QString& userId);
+ QNetworkReply* getRegions(const QString& userId);
+ QNetworkReply* getDevice(const QString& deviceId);
+
+ QNetworkReply* postDevice(const QByteArray& data);
+ QNetworkReply* postDeviceHeartbeat(const QString& deviceId, const QByteArray& data);
+ QNetworkReply* submitServerFeedback(const QString& serverId, const QByteArray& data);
+
+ QNetworkReply* updateServer(const QString& serverId, const QByteArray& data);
+ QNetworkReply* updateDevice(const QString& deviceId, const QByteArray& data);
+
+ QNetworkReply* deleteDevice(const QString& deviceId);
+
+ private:
+ QNetworkReply* get(const QUrl& url);
+ QNetworkReply* put(const QUrl& url, const QByteArray& data);
+ QNetworkReply* post(const QUrl& url, const QByteArray& data);
+ QNetworkReply* deleteResource(const QUrl& url);
+
+ QString m_accessToken;
+ QString m_apiHost;
+ QNetworkAccessManager* m_networkAccessManager;
+};
+
+#endif // VSAPI_H
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsAudio.cpp
+ * \author Matt Horton
+ * \date September 2022
+ */
+
+#include "vsAudio.h"
+
+#include <QDebug>
+#include <QEventLoop>
+#include <QJsonObject>
+#include <QSettings>
+#include <QThread>
+
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#endif
+
+#ifndef NO_JACK
+#include "../JackAudioInterface.h"
+#endif
+
+#ifdef __APPLE__
+#include "vsMacPermissions.h"
+#else
+#include "vsPermissions.h"
+#endif
+
+#ifndef NO_FEEDBACK
+#include "../Analyzer.h"
+#endif
+
+#include "../JackTrip.h"
+#include "../Meter.h"
+#include "../Monitor.h"
+#include "../Tone.h"
+#include "../Volume.h"
+#include "AudioInterfaceMode.h"
+
+// generic function to wait for a signal to be emitted
+template<typename SignalSenderPtr, typename SignalFuncPtr>
+static inline void WaitForSignal(SignalSenderPtr sender, SignalFuncPtr signal)
+{
+ QTimer timer;
+ timer.setTimerType(Qt::CoarseTimer);
+ timer.setSingleShot(true);
+
+ QEventLoop loop;
+ QObject::connect(sender, signal, &loop, &QEventLoop::quit);
+ QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ timer.start(10000); // wait for max 10 seconds
+ loop.exec();
+}
+
+// Constructor
+VsAudio::VsAudio(QObject* parent)
+ : QObject(parent)
+ , m_inputMeterLevels(2, 0)
+ , m_outputMeterLevels(2, 0)
+ , m_inputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+ , m_outputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+ , m_inputChannelsComboModel(
+ QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+ , m_outputChannelsComboModel(
+ QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+ , m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+ , m_audioWorkerPtr(new VsAudioWorker(this))
+ , m_workerThreadPtr(nullptr)
+ , m_inputMeterPluginPtr(nullptr)
+ , m_outputMeterPluginPtr(nullptr)
+ , m_inputVolumePluginPtr(nullptr)
+ , m_outputVolumePluginPtr(nullptr)
+ , m_monitorPluginPtr(nullptr)
+ , mHasErrors(false)
+{
+ loadSettings();
+
+ QJsonObject element;
+ element.insert(QString::fromStdString("label"), QString::fromStdString("Mono"));
+ element.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MONO));
+ m_inputMixModeComboModel = QJsonArray();
+ m_inputMixModeComboModel.push_back(element);
+
+ element = QJsonObject();
+ element.insert(QString::fromStdString("label"), QString::fromStdString("1"));
+ element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
+ element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
+ m_inputChannelsComboModel = QJsonArray();
+ m_inputChannelsComboModel.push_back(element);
+
+ element = QJsonObject();
+ element.insert(QString::fromStdString("label"), QString::fromStdString("1 & 2"));
+ element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
+ element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt());
+ m_outputChannelsComboModel = QJsonArray();
+ m_outputChannelsComboModel.push_back(element);
+
+ // Initialize timers needed for clip indicators
+ m_inputClipTimer.setTimerType(Qt::CoarseTimer);
+ m_inputClipTimer.setSingleShot(true);
+ m_inputClipTimer.setInterval(3000);
+ m_outputClipTimer.setTimerType(Qt::CoarseTimer);
+ m_outputClipTimer.setSingleShot(true);
+ m_outputClipTimer.setInterval(3000);
+ m_inputClipTimer.callOnTimeout([&]() {
+ if (m_inputClipped) {
+ m_inputClipped = false;
+ emit updatedInputClipped(m_inputClipped);
+ }
+ });
+ m_outputClipTimer.callOnTimeout([&]() {
+ if (m_outputClipped) {
+ m_outputClipped = false;
+ emit updatedOutputClipped(m_outputClipped);
+ }
+ });
+
+ // move audio worker to its own thread
+ 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(),
+ &VsAudioWorker::openAudioInterface, Qt::QueuedConnection);
+ connect(this, &VsAudio::signalStopAudio, m_audioWorkerPtr.get(),
+ &VsAudioWorker::closeAudioInterface, Qt::QueuedConnection);
+#ifdef RT_AUDIO
+ connect(this, &VsAudio::signalRefreshDevices, m_audioWorkerPtr.get(),
+ &VsAudioWorker::refreshDevices, Qt::QueuedConnection);
+ connect(this, &VsAudio::signalValidateDevices, m_audioWorkerPtr.get(),
+ &VsAudioWorker::validateDevices, Qt::QueuedConnection);
+ connect(m_audioWorkerPtr.get(), &VsAudioWorker::signalUpdatedDeviceModels, this,
+ &VsAudio::setDeviceModels, Qt::QueuedConnection);
+#endif
+
+ // Add permissions for Mac
+#ifdef __APPLE__
+ m_permissionsPtr.reset(new VsMacPermissions());
+ if (m_permissionsPtr->micPermissionChecked()
+ && m_permissionsPtr->micPermission() == "unknown") {
+ m_permissionsPtr->getMicPermission();
+ }
+#else
+ m_permissionsPtr.reset(new VsPermissions());
+#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<AudioInterfaceMode::JACK>()
+ || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool VsAudio::jackIsAvailable() const
+{
+ if constexpr (isBackendAvailable<AudioInterfaceMode::JACK>()) {
+#ifdef USE_WEAK_JACK
+ // Check if Jack is available
+ return (have_libjack() == 0);
+#else
+ return true;
+#endif
+ } else {
+ return false;
+ }
+}
+
+bool VsAudio::asioIsAvailable() const
+{
+#if defined(RT_AUDIO) && defined(_WIN32)
+ return RtAudio::getCompiledApiByName("asio") != RtAudio::UNSPECIFIED;
+#else
+ return false;
+#endif
+}
+
+void VsAudio::setAudioReady(bool ready)
+{
+ if (ready == m_audioReady)
+ return;
+ m_audioReady = ready;
+ emit signalAudioReadyChanged();
+ if (m_audioReady)
+ emit signalAudioIsReady();
+ else
+ emit signalAudioIsNotReady();
+}
+
+void VsAudio::setScanningDevices(bool b)
+{
+ if (b == m_scanningDevices)
+ return;
+ m_scanningDevices = b;
+ emit signalScanningDevicesChanged();
+}
+
+void VsAudio::setAudioBackend(const QString& backend)
+{
+ bool useRtAudio = (backend == QStringLiteral("RtAudio"));
+ if (useRtAudio) {
+ if (getUseRtAudio())
+ return;
+ m_backend = AudioBackendType::RTAUDIO;
+ } else {
+ if (!getUseRtAudio())
+ return;
+ m_backend = AudioBackendType::JACK;
+ }
+ emit audioBackendChanged(useRtAudio);
+}
+
+void VsAudio::setFeedbackDetectionEnabled(bool enabled)
+{
+ if (m_feedbackDetectionEnabled == enabled)
+ return;
+ m_feedbackDetectionEnabled = enabled;
+ emit feedbackDetectionEnabledChanged();
+}
+
+void VsAudio::setSampleRate(int sampleRate)
+{
+ if (m_audioSampleRate == sampleRate)
+ return;
+ m_audioSampleRate = sampleRate;
+ emit sampleRateChanged();
+}
+
+void VsAudio::setBufferSize(int bufSize)
+{
+ if (m_audioBufferSize == bufSize)
+ return;
+ m_audioBufferSize = bufSize;
+ emit bufferSizeChanged();
+}
+
+void VsAudio::setQueueBuffer(int queueBuffer)
+{
+ if (m_queueBuffer == queueBuffer)
+ return;
+ if (queueBuffer < 0)
+ queueBuffer = 0;
+ m_queueBuffer = queueBuffer;
+ emit queueBufferChanged();
+}
+
+void VsAudio::setNumInputChannels(int numChannels)
+{
+ if (numChannels == m_numInputChannels)
+ return;
+ m_numInputChannels = numChannels;
+ emit numInputChannelsChanged(numChannels);
+}
+
+void VsAudio::setNumOutputChannels(int numChannels)
+{
+ if (numChannels == m_numOutputChannels)
+ return;
+ m_numOutputChannels = numChannels;
+ emit numOutputChannelsChanged(numChannels);
+}
+
+void VsAudio::setBaseInputChannel(int baseChannel)
+{
+ if (baseChannel == m_baseInputChannel)
+ return;
+ m_baseInputChannel = baseChannel;
+ emit baseInputChannelChanged(baseChannel);
+ return;
+}
+
+void VsAudio::setBaseOutputChannel(int baseChannel)
+{
+ if (baseChannel == m_baseOutputChannel)
+ return;
+ m_baseOutputChannel = baseChannel;
+ emit baseOutputChannelChanged(baseChannel);
+ return;
+}
+
+void VsAudio::setInputMixMode(const int mode)
+{
+ if (mode == m_inputMixMode)
+ return;
+ m_inputMixMode = mode;
+ emit inputMixModeChanged(mode);
+ return;
+}
+
+void VsAudio::setInputMuted(bool muted)
+{
+ if (m_inMuted == muted)
+ return;
+ m_inMuted = muted;
+ emit updatedInputMuted(muted);
+}
+
+void VsAudio::setInputVolume(float multiplier)
+{
+ if (multiplier == m_inMultiplier)
+ return;
+ m_inMultiplier = multiplier;
+ emit updatedInputVolume(multiplier);
+}
+
+void VsAudio::setOutputVolume(float multiplier)
+{
+ if (multiplier == m_outMultiplier)
+ return;
+ m_outMultiplier = multiplier;
+ emit updatedOutputVolume(multiplier);
+}
+
+void VsAudio::setMonitorVolume(float multiplier)
+{
+ if (multiplier == m_monMultiplier)
+ return;
+ m_monMultiplier = multiplier;
+ emit updatedMonitorVolume(multiplier);
+}
+
+void VsAudio::setInputDevice([[maybe_unused]] const QString& device)
+{
+ if (!getUseRtAudio())
+ return;
+#ifdef RT_AUDIO
+ if (device == m_inputDevice)
+ return;
+ m_inputDevice = device;
+ emit inputDeviceChanged(m_inputDevice);
+#endif
+}
+
+void VsAudio::setOutputDevice([[maybe_unused]] const QString& device)
+{
+ if (!getUseRtAudio())
+ return;
+#ifdef RT_AUDIO
+ if (device == m_outputDevice)
+ return;
+ m_outputDevice = device;
+ emit outputDeviceChanged(m_outputDevice);
+#endif
+}
+
+void VsAudio::setDevicesErrorMsg(const QString& msg)
+{
+ if (m_devicesErrorMsg == msg)
+ return;
+ m_devicesErrorMsg = msg;
+ emit devicesErrorChanged();
+}
+
+void VsAudio::setDevicesWarningMsg(const QString& msg)
+{
+ if (m_devicesWarningMsg == msg)
+ return;
+ m_devicesWarningMsg = msg;
+ emit devicesWarningChanged();
+}
+
+void VsAudio::setDevicesErrorHelpUrl(const QString& url)
+{
+ if (m_devicesErrorHelpUrl == url)
+ return;
+ m_devicesErrorHelpUrl = url;
+ emit devicesErrorHelpUrlChanged();
+}
+
+void VsAudio::setDevicesWarningHelpUrl(const QString& url)
+{
+ if (m_devicesWarningHelpUrl == url)
+ return;
+ m_devicesWarningHelpUrl = url;
+ emit devicesWarningHelpUrlChanged();
+}
+
+void VsAudio::setHighLatencyFlag(bool highLatencyFlag)
+{
+ if (m_highLatencyFlag == highLatencyFlag)
+ return;
+ m_highLatencyFlag = highLatencyFlag;
+ emit highLatencyFlagChanged(highLatencyFlag);
+}
+
+void VsAudio::startAudio(bool block)
+{
+ // note this is also used for restartAudio()
+ emit signalStartAudio();
+ if (!block)
+ return;
+ WaitForSignal(this, &VsAudio::signalAudioIsReady);
+}
+
+void VsAudio::stopAudio(bool block)
+{
+ if (!getAudioReady())
+ return;
+ emit signalStopAudio();
+ if (!block)
+ return;
+ WaitForSignal(this, &VsAudio::signalAudioIsNotReady);
+}
+
+void VsAudio::refreshDevices(bool block)
+{
+ if (!getUseRtAudio())
+ return;
+ emit signalRefreshDevices();
+ if (!block)
+ return;
+ WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
+}
+
+void VsAudio::validateDevices(bool block)
+{
+ if (!getUseRtAudio())
+ return;
+ emit signalValidateDevices();
+ if (!block)
+ return;
+ WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
+}
+
+void VsAudio::loadSettings()
+{
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("Audio"));
+ setInputVolume(settings.value(QStringLiteral("InMultiplier"), 1).toFloat());
+ setOutputVolume(settings.value(QStringLiteral("OutMultiplier"), 1).toFloat());
+ setMonitorVolume(settings.value(QStringLiteral("MonMultiplier"), 0).toFloat());
+ // note: we should always reset input muted to false; otherwise, bad things
+ setInputMuted(false);
+ // setInputMuted(settings.value(QStringLiteral("InMuted"), false).toBool());
+
+ // load audio backend
+ AudioBackendType audioBackend;
+ if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()) {
+ audioBackend =
+ (settings.value(QStringLiteral("Backend"), AudioBackendType::RTAUDIO).toInt()
+ == 1)
+ ? AudioBackendType::RTAUDIO
+ : AudioBackendType::JACK;
+ } else if constexpr (isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
+ audioBackend = AudioBackendType::RTAUDIO;
+ } else {
+ audioBackend = AudioBackendType::JACK;
+ }
+ if (audioBackend != m_backend) {
+ setAudioBackend(audioBackend == AudioBackendType::RTAUDIO
+ ? QStringLiteral("RtAudio")
+ : QStringLiteral("JACK"));
+ }
+
+ // load input and output devices
+ QString inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString();
+ QString outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString();
+ if (inputDevice == QStringLiteral("(default)")) {
+ inputDevice = "";
+ }
+ if (outputDevice == QStringLiteral("(default)")) {
+ outputDevice = "";
+ }
+ setInputDevice(inputDevice);
+ setOutputDevice(outputDevice);
+
+ // use default base channel 0, if the setting does not exist
+ setBaseInputChannel(settings.value(QStringLiteral("BaseInputChannel"), 0).toInt());
+ setBaseOutputChannel(settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt());
+
+ // Handle migration scenarios. Assume this is a new user
+ // if we have m_inputDevice == "" and m_outputDevice == ""
+ if (m_inputDevice == "" && m_outputDevice == "") {
+ // for fresh installs, use mono by default
+ setNumInputChannels(
+ settings.value(QStringLiteral("NumInputChannels"), 1).toInt());
+ setInputMixMode(settings
+ .value(QStringLiteral("InputMixMode"),
+ static_cast<int>(AudioInterface::MONO))
+ .toInt());
+
+ // use 2 channels for output
+ setNumOutputChannels(
+ settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
+ } else {
+ // existing installs - keep using stereo
+ setNumInputChannels(
+ settings.value(QStringLiteral("NumInputChannels"), 2).toInt());
+ setInputMixMode(settings
+ .value(QStringLiteral("InputMixMode"),
+ static_cast<int>(AudioInterface::STEREO))
+ .toInt());
+
+ // use 2 channels for output
+ setNumOutputChannels(
+ settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
+ }
+
+ setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt());
+ setQueueBuffer(settings.value(QStringLiteral("QueueBuffer"), 0).toInt());
+ setFeedbackDetectionEnabled(
+ settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool());
+ settings.endGroup();
+}
+
+void VsAudio::saveSettings()
+{
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("Audio"));
+ settings.setValue(QStringLiteral("InMultiplier"), m_inMultiplier);
+ settings.setValue(QStringLiteral("OutMultiplier"), m_outMultiplier);
+ settings.setValue(QStringLiteral("MonMultiplier"), m_monMultiplier);
+ // settings.setValue(QStringLiteral("InMuted"), m_inMuted);
+ settings.setValue(QStringLiteral("Backend"), getUseRtAudio() ? 1 : 0);
+ settings.setValue(QStringLiteral("InputDevice"), m_inputDevice);
+ settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice);
+ settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel);
+ settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels);
+ settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode);
+ settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel);
+ settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels);
+ settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize);
+ settings.setValue(QStringLiteral("QueueBuffer"), m_queueBuffer);
+ settings.setValue(QStringLiteral("FeedbackDetectionEnabled"),
+ m_feedbackDetectionEnabled);
+ settings.endGroup();
+}
+
+void VsAudio::detectedFeedbackLoop()
+{
+ setInputMuted(true);
+ setMonitorVolume(0);
+ emit feedbackDetected();
+}
+
+void VsAudio::updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels)
+{
+ 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 < numChannels) {
+ dB = std::max(m_meterMin, valuesInDecibels[i]);
+ }
+
+ // Produce a normalized value from 0 to 1
+ m_inputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+ // Signal a clip if we haven't done so already
+ if (dB >= -0.05 && !detectedClip) {
+ m_inputClipTimer.start();
+ m_inputClipped = true;
+ emit updatedInputClipped(m_inputClipped);
+ detectedClip = true;
+ }
+ }
+
+#ifdef RT_AUDIO
+ // For certain specific cases, copy the first channel's value into the second
+ // channel's value
+ if (getUseRtAudio()
+ && ((m_inputMixMode == static_cast<int>(AudioInterface::MONO)
+ && m_numInputChannels == 1)
+ || (m_inputMixMode == static_cast<int>(AudioInterface::MIXTOMONO)
+ && m_numInputChannels == 2))) {
+ m_inputMeterLevels[1] = m_inputMeterLevels[0];
+ }
+#endif
+
+ emit updatedInputMeterLevels(m_inputMeterLevels);
+}
+
+void VsAudio::updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels)
+{
+ 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 < numChannels) {
+ dB = std::max(m_meterMin, valuesInDecibels[i]);
+ }
+
+ // Produce a normalized value from 0 to 1
+ m_outputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+ // Signal a clip if we haven't done so already
+ if (dB >= -0.05 && !detectedClip) {
+ m_outputClipTimer.start();
+ m_outputClipped = true;
+ emit updatedOutputClipped(m_outputClipped);
+ detectedClip = true;
+ }
+ }
+#ifdef RT_AUDIO
+ if (m_numOutputChannels == 1) {
+ m_outputMeterLevels[1] = m_outputMeterLevels[0];
+ }
+#endif
+ emit updatedOutputMeterLevels(m_outputMeterLevels);
+}
+
+void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
+ int numInputChannels, int numOutputChannels)
+{
+ // Make sure clip timers are stopped
+ m_inputClipTimer.stop();
+ m_outputClipTimer.stop();
+
+ // Reset meters
+ m_inputMeterLevels[0] = m_inputMeterLevels[1] = 0;
+ m_outputMeterLevels[0] = m_outputMeterLevels[1] = 0;
+ m_inputClipped = m_outputClipped = false;
+ emit updatedInputMeterLevels(m_inputMeterLevels);
+ emit updatedOutputMeterLevels(m_outputMeterLevels);
+ emit updatedInputClipped(m_inputClipped);
+ emit updatedOutputClipped(m_outputClipped);
+ setInputMuted(false);
+
+ // Create plugins
+ m_inputMeterPluginPtr = new Meter(numInputChannels);
+ m_outputMeterPluginPtr = new Meter(numOutputChannels);
+ m_inputVolumePluginPtr = new Volume(numInputChannels);
+ m_outputVolumePluginPtr = new Volume(numOutputChannels);
+
+ // initialize input and output volumes
+ m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
+ m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
+ m_inputVolumePluginPtr->muteUpdated(m_inMuted);
+
+ // Connect plugins for communication with UI
+ connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+ &VsAudio::updatedInputVuMeasurements);
+ connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+ &VsAudio::updatedOutputVuMeasurements);
+ connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr,
+ &Volume::volumeUpdated);
+ connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr,
+ &Volume::volumeUpdated);
+ connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr,
+ &Volume::muteUpdated);
+
+ // Note that plugin ownership is passed to the JackTrip class
+ // In particular, the AudioInterface that it uses to connect
+ audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr);
+ audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr);
+
+ if (forJackTrip) {
+ // plugins for stream going to audio interface
+ audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+
+ // Setup monitor
+ // Note: Constructor determines how many internal monitor buffers to allocate
+ m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels));
+ m_monitorPluginPtr->volumeUpdated(m_monMultiplier);
+ audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr);
+ connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr,
+ &Monitor::volumeUpdated);
+
+#ifndef NO_FEEDBACK
+ // Setup output analyzer
+ if (m_feedbackDetectionEnabled) {
+ m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
+ m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
+ audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr);
+ connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
+ &VsAudio::detectedFeedbackLoop);
+ }
+#endif
+
+ // Setup output meter
+ // Note: Add this to monitor process to include self-volume
+ m_outputMeterPluginPtr->setIsMonitoringMeter(true);
+ audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr);
+
+ } else {
+ // tone plugin is used to test audio output
+ Tone* outputTonePluginPtr = new Tone(getNumOutputChannels());
+ connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr,
+ &Tone::triggerPlayback);
+ audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr);
+
+ // plugins for stream going to audio interface
+ audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+ audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr);
+ }
+}
+
+void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel)
+{
+ m_inputComboModel = inputComboModel;
+ m_outputComboModel = outputComboModel;
+ emit inputComboModelChanged();
+ emit outputComboModelChanged();
+ if (!m_deviceModelsInitialized) {
+ m_deviceModelsInitialized = true;
+ emit deviceModelsInitializedChanged(true);
+ }
+}
+
+void VsAudio::setInputChannelsComboModel(QJsonArray& model)
+{
+ m_inputChannelsComboModel = model;
+ emit inputChannelsComboModelChanged();
+}
+
+void VsAudio::setOutputChannelsComboModel(QJsonArray& model)
+{
+ m_outputChannelsComboModel = model;
+ emit outputChannelsComboModelChanged();
+}
+
+void VsAudio::setInputMixModeComboModel(QJsonArray& model)
+{
+ m_inputMixModeComboModel = model;
+ emit inputMixModeComboModelChanged();
+}
+
+void VsAudio::updateDeviceMessages(AudioInterface& audioInterface)
+{
+ QString devicesWarningMsg =
+ QString::fromStdString(audioInterface.getDevicesWarningMsg());
+ QString devicesErrorMsg = QString::fromStdString(audioInterface.getDevicesErrorMsg());
+ QString devicesWarningHelpUrl =
+ QString::fromStdString(audioInterface.getDevicesWarningHelpUrl());
+ QString devicesErrorHelpUrl =
+ QString::fromStdString(audioInterface.getDevicesErrorHelpUrl());
+
+ if (devicesWarningMsg != "") {
+ qDebug() << "Devices Warning: " << devicesWarningMsg;
+ if (devicesWarningHelpUrl != "") {
+ qDebug() << "Learn More: " << devicesWarningHelpUrl;
+ }
+ }
+
+ if (devicesErrorMsg != "") {
+ qDebug() << "Devices Error: " << devicesErrorMsg;
+ if (devicesErrorHelpUrl != "") {
+ qDebug() << "Learn More: " << devicesErrorHelpUrl;
+ }
+ }
+
+ setDevicesWarningMsg(devicesWarningMsg);
+ setDevicesErrorMsg(devicesErrorMsg);
+ setDevicesWarningHelpUrl(devicesWarningHelpUrl);
+ setDevicesErrorHelpUrl(devicesErrorHelpUrl);
+ setHighLatencyFlag(audioInterface.getHighLatencyFlag());
+}
+
+AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr)
+{
+ // Create AudioInterface Client Object
+ AudioInterface* ifPtr = nullptr;
+ if (m_backend == VsAudio::AudioBackendType::JACK) {
+ if (!isBackendAvailable<AudioInterfaceMode::JACK>()) {
+ throw std::runtime_error(
+ "JackTrip was not compiled with support for the Jack backend. "
+ "In order to use Jack, you'll need to "
+ "rebuild with Jack support.");
+ }
+ if (!jackIsAvailable()) {
+ throw std::runtime_error(
+ "Unable to load the Jack client library. "
+ "In order to use Jack, you'll need to first install it.");
+ }
+ qDebug() << "Using JACK backend";
+ ifPtr = newJackAudioInterface(jackTripPtr);
+ } else if (m_backend == VsAudio::AudioBackendType::RTAUDIO) {
+ if (!isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
+ throw std::runtime_error(
+ "JackTrip was not compiled with support for the RtAudio backend. "
+ "In order to use RtAudio, you'll need to "
+ "rebuild with RtAudio support.");
+ }
+ qDebug() << "Using RtAudio backend";
+ ifPtr = newRtAudioInterface(jackTripPtr);
+ } else {
+ throw std::runtime_error("Unknown audio backend");
+ }
+
+ mHasErrors = false;
+ ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) {
+ this->errorCallback(errorText, jackTripPtr);
+ });
+
+ // AudioInterface::setup() can return a different buffer size
+ // if the audio interface doesn't support the one that was requested
+ if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) {
+ setBufferSize(ifPtr->getBufferSizeInSamples());
+ }
+
+ std::cout << "The Sampling Rate is: " << ifPtr->getSampleRate() << std::endl;
+ std::cout << gPrintSeparator << std::endl;
+ int AudioBufferSizeInBytes = ifPtr->getBufferSizeInSamples() * sizeof(sample_t);
+ std::cout << "The Audio Buffer Size is: " << ifPtr->getBufferSizeInSamples()
+ << " samples" << std::endl;
+ std::cout << " or: " << AudioBufferSizeInBytes << " bytes"
+ << std::endl;
+ std::cout << gPrintSeparator << std::endl;
+ std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels()
+ << std::endl;
+ std::cout << gPrintSeparator << std::endl;
+
+ // setup audio plugins
+ appendProcessPlugins(*ifPtr, jackTripPtr != nullptr, getNumInputChannels(),
+ getNumOutputChannels());
+
+ return ifPtr;
+}
+
+AudioInterface* VsAudio::newJackAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
+{
+ AudioInterface* ifPtr = nullptr;
+#ifndef NO_JACK
+ static const int numJackChannels = 2;
+ if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()
+ || isBackendAvailable<AudioInterfaceMode::JACK>()) {
+ QVarLengthArray<int> inputChans;
+ QVarLengthArray<int> outputChans;
+ inputChans.resize(numJackChannels);
+ outputChans.resize(numJackChannels);
+
+ for (int i = 0; i < numJackChannels; i++) {
+ inputChans[i] = 1 + i;
+ }
+ for (int i = 0; i < numJackChannels; i++) {
+ outputChans[i] = 1 + i;
+ }
+
+ ifPtr = new JackAudioInterface(inputChans, outputChans, m_audioBitResolution,
+ jackTripPtr != nullptr, jackTripPtr);
+ ifPtr->setClientName(QStringLiteral("JackTrip"));
+#if defined(__unix__)
+ AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate());
+#endif
+ ifPtr->setup(true);
+ }
+#endif
+ return ifPtr;
+}
+
+AudioInterface* VsAudio::newRtAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
+{
+ AudioInterface* ifPtr = nullptr;
+#ifdef RT_AUDIO
+ QVarLengthArray<int> inputChans;
+ QVarLengthArray<int> outputChans;
+ inputChans.resize(getNumInputChannels());
+ outputChans.resize(getNumOutputChannels());
+
+ for (int i = 0; i < getNumInputChannels(); i++) {
+ inputChans[i] = getBaseInputChannel() + i;
+ }
+ for (int i = 0; i < getNumOutputChannels(); i++) {
+ outputChans[i] = getBaseOutputChannel() + i;
+ }
+
+ ifPtr = new RtAudioInterface(
+ inputChans, outputChans,
+ static_cast<AudioInterface::inputMixModeT>(getInputMixMode()),
+ m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr);
+ ifPtr->setSampleRate(getSampleRate());
+ ifPtr->setInputDevice(getInputDevice().toStdString());
+ ifPtr->setOutputDevice(getOutputDevice().toStdString());
+ ifPtr->setBufferSizeInSamples(getBufferSize());
+
+ QVector<RtAudioDevice> devices = m_audioWorkerPtr->getDevices();
+ if (!devices.empty())
+ static_cast<RtAudioInterface*>(ifPtr)->setRtAudioDevices(devices);
+
+#if defined(__unix__)
+ AudioInterface::setPipewireLatency(getBufferSize(), ifPtr->getSampleRate());
+#endif
+
+ // Note: setup might change the number of channels and/or buffer size
+ ifPtr->setup(true);
+
+ // TODO: Add check for if base input channel needs to change
+ if (jackTripPtr != nullptr && getNumInputChannels() == 2
+ && getInputMixMode() == AudioInterface::MIXTOMONO)
+ jackTripPtr->setNumInputChannels(1);
+
+#endif // RT_AUDIO
+ return ifPtr;
+}
+
+void VsAudio::errorCallback(const std::string& errorText,
+ [[maybe_unused]] JackTrip* jackTripPtr)
+{
+ const QString errorMsg(QString::fromStdString(errorText));
+ setDevicesErrorMsg(errorMsg);
+#ifdef _WIN32
+ // handle special case for Windows ASIO drivers that trigger
+ // asynchronous errors shortly after you try opening the
+ // RtAudio stream with a different sample rate (only for audio tester)
+ if (jackTripPtr == nullptr && getUseRtAudio()
+ && errorMsg.contains("sample rate changed")) {
+ // only refresh devices once
+ if (mHasErrors)
+ return;
+ mHasErrors = true;
+ // asynchronously refresh devices
+ refreshDevices(false);
+ }
+#else
+ mHasErrors = true;
+#endif
+}
+
+// VsAudioWorker methods
+
+VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {}
+
+void VsAudioWorker::openAudioInterface()
+{
+#ifdef __APPLE__
+ if (m_parentPtr->m_permissionsPtr->micPermission() != "granted") {
+ return;
+ }
+#endif
+
+ if constexpr (!(isBackendAvailable<AudioInterfaceMode::JACK>()
+ || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
+ return;
+ }
+
+ if (!m_audioInterfacePtr.isNull()) {
+ std::cout << "Restarting Audio" << std::endl;
+ closeAudioInterface();
+ } else {
+ std::cout << "Starting Audio" << std::endl;
+ }
+
+ unsigned int maxTries = 2;
+#ifdef RT_AUDIO
+ // Update devices, if not already initialized
+ if (getUseRtAudio()) {
+ if (!m_parentPtr->getDeviceModelsInitialized()) {
+ updateDeviceModels();
+ maxTries = 1;
+ }
+ }
+#endif
+ for (unsigned int tryNum = 0; tryNum < maxTries; ++tryNum) {
+#ifdef RT_AUDIO
+ if (tryNum > 0) {
+ if (getUseRtAudio()) {
+ updateDeviceModels();
+ } else {
+ m_parentPtr->setAudioBackend("RtAudio");
+ updateDeviceModels();
+ }
+ }
+#endif
+ try {
+ // create and setup a new audio interface
+ m_audioInterfacePtr.reset(m_parentPtr->newAudioInterface());
+ // success if it doesn't throw
+ break;
+ } catch (const std::exception& e) {
+ emit signalError(QString::fromUtf8(e.what()));
+ }
+ }
+
+ if (m_audioInterfacePtr.isNull()) {
+ return;
+ }
+
+ // initialize plugins and start the audio callback process
+ m_audioInterfacePtr->initPlugins(false);
+ m_audioInterfacePtr->startProcess();
+ m_audioInterfacePtr->connectDefaultPorts();
+
+ m_parentPtr->updateDeviceMessages(*m_audioInterfacePtr);
+ m_parentPtr->setAudioReady(true);
+}
+
+void VsAudioWorker::closeAudioInterface()
+{
+ if (m_audioInterfacePtr.isNull())
+ return;
+ std::cout << "Stopping Audio" << std::endl;
+ try {
+ m_audioInterfacePtr->stopProcess();
+ } catch (const std::exception& e) {
+ emit signalError(QString::fromUtf8(e.what()));
+ }
+ m_audioInterfacePtr.clear();
+ m_parentPtr->setAudioReady(false);
+}
+
+#ifdef RT_AUDIO
+
+void VsAudioWorker::refreshDevices()
+{
+ if (!getUseRtAudio())
+ return;
+ bool restartAudio = !m_audioInterfacePtr.isNull();
+ if (restartAudio)
+ closeAudioInterface();
+ updateDeviceModels();
+ if (restartAudio)
+ openAudioInterface();
+}
+
+void VsAudioWorker::updateDeviceModels()
+{
+ if (!getUseRtAudio())
+ return;
+
+ // note: audio must not be active when scanning devices
+ m_parentPtr->setScanningDevices(true);
+ closeAudioInterface();
+ RtAudioInterface::scanDevices(m_devices);
+
+ QStringList inputDeviceCategories;
+ QStringList outputDeviceCategories;
+
+ getDeviceList(m_devices, m_inputDeviceList, inputDeviceCategories,
+ m_inputDeviceChannels, true);
+ getDeviceList(m_devices, m_outputDeviceList, outputDeviceCategories,
+ m_outputDeviceChannels, false);
+
+ QJsonArray inputComboModel =
+ formatDeviceList(m_inputDeviceList, inputDeviceCategories, m_inputDeviceChannels);
+ QJsonArray outputComboModel = formatDeviceList(
+ m_outputDeviceList, outputDeviceCategories, m_outputDeviceChannels);
+
+ validateDevices();
+
+ // let VsAudio know that things have been updated
+ m_parentPtr->setScanningDevices(false);
+ emit signalUpdatedDeviceModels(inputComboModel, outputComboModel);
+}
+
+void VsAudioWorker::getDeviceList(const QVector<RtAudioDevice>& devices,
+ QStringList& list, QStringList& categories,
+ QList<int>& channels, bool isInput)
+{
+ categories.clear();
+ channels.clear();
+ list.clear();
+
+ // do not include blacklisted audio interfaces
+ // these are known to be unstable and cause JackTrip to crash
+ QVector<QString> blacklisted_devices = {
+#ifdef _WIN32
+ // Realtek ASIO: seems to crash any computer that tries to use it
+ QString::fromUtf8("Realtek ASIO"),
+ QString::fromUtf8("Generic Low Latency ASIO Driver"),
+#endif
+ // JackRouter: crashes if not running; use Jack backend instead
+ QString::fromUtf8("JackRouter"),
+ };
+
+ for (int n = 0; n < devices.size(); ++n) {
+#ifdef _WIN32
+ if (devices[n].api == RtAudio::UNIX_JACK) {
+ continue;
+ }
+#endif
+ const QString deviceName(QString::fromStdString(devices[n].name));
+
+ // Don't include duplicate entries
+ if (list.contains(deviceName)) {
+ continue;
+ }
+
+ // Skip if no channels available
+ if ((isInput && devices[n].inputChannels == 0)
+ || (!isInput && devices[n].outputChannels == 0)) {
+ continue;
+ }
+
+ // Skip blacklisted devices
+ const bool iPhoneMic = deviceName.startsWith("Apple Inc.:")
+ && deviceName.endsWith("Phone Microphone");
+ if (blacklisted_devices.contains(deviceName) || iPhoneMic) {
+ std::cout << "RTAudio: blacklisted " << (isInput ? "input" : "output")
+ << " device: " << devices[n].name << std::endl;
+ continue;
+ }
+
+ // Good to go!
+ if (isInput) {
+ list.append(deviceName);
+ channels.append(devices[n].inputChannels);
+ } else {
+ list.append(deviceName);
+ channels.append(devices[n].outputChannels);
+ }
+
+ switch (devices[n].api) {
+ case RtAudio::WINDOWS_ASIO:
+ categories.append("Low-Latency (ASIO)");
+ break;
+ case RtAudio::WINDOWS_WASAPI:
+ categories.append("High-Latency (WASAPI)");
+ break;
+ case RtAudio::WINDOWS_DS:
+ categories.append("High-Latency (DirectSound)");
+ break;
+ case RtAudio::LINUX_ALSA:
+ categories.append("Low-Latency (ALSA)");
+ break;
+ case RtAudio::LINUX_PULSE:
+ categories.append("High-Latency (Pulse)");
+ break;
+ case RtAudio::LINUX_OSS:
+ categories.append("High-Latency (OSS)");
+ break;
+ default:
+ categories.append("");
+ break;
+ }
+ }
+}
+
+QJsonArray VsAudioWorker::formatDeviceList(const QStringList& devices,
+ const QStringList& categories,
+ const QList<int>& channels)
+{
+ QStringList uniqueCategories = QStringList(categories);
+ uniqueCategories.removeDuplicates();
+
+ bool containsCategories = true;
+ if (uniqueCategories.size() == 0) {
+ containsCategories = false;
+ } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") {
+ containsCategories = false;
+ }
+
+ QJsonArray items;
+ for (int i = 0; i < uniqueCategories.size(); i++) {
+ QString category = uniqueCategories.at(i);
+
+ if (containsCategories) {
+ QJsonObject header = QJsonObject();
+ header.insert(QString::fromStdString("text"), category);
+ header.insert(QString::fromStdString("type"),
+ QString::fromStdString("header"));
+ header.insert(QString::fromStdString("category"), category);
+ items.push_back(header);
+ }
+
+ for (int j = 0; j < devices.size(); j++) {
+ if (categories.at(j).toStdString() == category.toStdString()) {
+ QJsonObject element = QJsonObject();
+ element.insert(QString::fromStdString("text"), devices.at(j));
+ element.insert(QString::fromStdString("type"),
+ QString::fromStdString("element"));
+ element.insert(QString::fromStdString("channels"), channels.at(j));
+ element.insert(QString::fromStdString("category"), category);
+ items.push_back(element);
+ }
+ }
+ }
+
+ return items;
+}
+
+void VsAudioWorker::validateDevices()
+{
+ if (!getUseRtAudio())
+ return;
+ validateInputDevicesState();
+ validateOutputDevicesState();
+ emit signalDevicesValidated();
+}
+
+void VsAudioWorker::validateInputDevicesState()
+{
+ if (!getUseRtAudio()) {
+ return;
+ }
+ if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
+ return;
+ }
+
+ // Given input device list, check that the currently set device
+ // actually exists
+ if (getInputDevice() == QStringLiteral("")
+ || m_inputDeviceList.indexOf(getInputDevice()) == -1) {
+ m_parentPtr->setInputDevice(m_inputDeviceList[0]);
+ }
+
+ // Given the currently selected input device, reset the available input channel
+ // options
+ int indexOfInput = m_inputDeviceList.indexOf(getInputDevice());
+ if (indexOfInput == -1) {
+ std::cerr << "Invalid state. Input device index should never be -1" << std::endl;
+ return;
+ }
+
+ int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput);
+ if (numDevicesChannelsAvailable < 1) {
+ std::cerr << "Invalid state. Number of channels should never be less than 1"
+ << std::endl;
+ return;
+ } else if (numDevicesChannelsAvailable == 1) {
+ // Set the input mix mode to just have "Mono" as the option
+ QJsonObject inputMixModeComboElement = QJsonObject();
+ inputMixModeComboElement.insert(QString::fromStdString("label"),
+ QString::fromStdString("Mono"));
+ inputMixModeComboElement.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MONO));
+ QJsonArray inputMixModeComboModel;
+ inputMixModeComboModel.push_back(inputMixModeComboElement);
+ m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+ // Set the input channels combo to only have channel 1 as an option
+ QJsonObject inputChannelsComboElement;
+ inputChannelsComboElement.insert(QString::fromStdString("label"),
+ QString::fromStdString("1"));
+ inputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
+ QVariant(0).toInt());
+ inputChannelsComboElement.insert(QString::fromStdString("numChannels"),
+ QVariant(1).toInt());
+ QJsonArray inputChannelsComboModel;
+ inputChannelsComboModel.push_back(inputChannelsComboElement);
+ m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
+
+ // Set the only allowed options for these variables automatically
+ m_parentPtr->setBaseInputChannel(0);
+ m_parentPtr->setNumInputChannels(1);
+ m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
+ } else {
+ // set the input channels selector to have the options based on the currently
+ // selected device
+ QJsonArray inputChannelsComboModel;
+ for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+ QJsonObject element = QJsonObject();
+ element.insert(QString::fromStdString("label"), QVariant(i + 1).toString());
+ element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt());
+ element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
+ inputChannelsComboModel.push_back(element);
+ }
+ for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+ if (i % 2 == 0) {
+ QJsonObject element = QJsonObject();
+ element.insert(
+ QString::fromStdString("label"),
+ QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+ element.insert(QString::fromStdString("baseChannel"),
+ QVariant(i).toInt());
+ element.insert(QString::fromStdString("numChannels"),
+ QVariant(2).toInt());
+ inputChannelsComboModel.push_back(element);
+ }
+ }
+ m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
+
+ // if the current m_baseInputChannel or m_numInputChannels is invalid based on
+ // this device's option, use the first two channels by default
+ if (getBaseInputChannel() + getNumInputChannels() > numDevicesChannelsAvailable) {
+ // we're in the case where numDevicesChannelsAvailable >= 2, so always have
+ // the ability to use the first 2 channels
+ m_parentPtr->setBaseInputChannel(0);
+ m_parentPtr->setNumInputChannels(2);
+ }
+ if (getNumInputChannels() != 1) {
+ // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if
+ // we're using 2 channels
+ QJsonObject inputMixModeComboElement1 = QJsonObject();
+ inputMixModeComboElement1.insert(QString::fromStdString("label"),
+ QString::fromStdString("Stereo"));
+ inputMixModeComboElement1.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::STEREO));
+ QJsonObject inputMixModeComboElement2 = QJsonObject();
+ inputMixModeComboElement2.insert(QString::fromStdString("label"),
+ QString::fromStdString("Mix to Mono"));
+ inputMixModeComboElement2.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MIXTOMONO));
+ QJsonArray inputMixModeComboModel;
+ inputMixModeComboModel.push_back(inputMixModeComboElement1);
+ inputMixModeComboModel.push_back(inputMixModeComboElement2);
+ m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+ // if m_inputMixMode is an invalid value, set it to "stereo" by default
+ // given that we are using 2 channels
+ if (getInputMixMode() != static_cast<int>(AudioInterface::STEREO)
+ && getInputMixMode() != static_cast<int>(AudioInterface::MIXTOMONO)) {
+ m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::STEREO));
+ }
+ } else {
+ // Set the input mix mode to just have "Mono" as the option if we're using 1
+ // channel
+ QJsonObject inputMixModeComboElement = QJsonObject();
+ inputMixModeComboElement.insert(QString::fromStdString("label"),
+ QString::fromStdString("Mono"));
+ inputMixModeComboElement.insert(QString::fromStdString("value"),
+ static_cast<int>(AudioInterface::MONO));
+ QJsonArray inputMixModeComboModel;
+ inputMixModeComboModel.push_back(inputMixModeComboElement);
+ m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+ // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO
+ if (getInputMixMode() != static_cast<int>(AudioInterface::MONO)) {
+ m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
+ }
+ }
+ }
+}
+
+void VsAudioWorker::validateOutputDevicesState()
+{
+ if (!getUseRtAudio()) {
+ return;
+ }
+ if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
+ return;
+ }
+
+ // Given output device list, check that the currently set device
+ // actually exists
+ if (getOutputDevice() == QStringLiteral("")
+ || m_outputDeviceList.indexOf(getOutputDevice()) == -1) {
+ m_parentPtr->setOutputDevice(m_outputDeviceList[0]);
+ }
+
+ // Given the currently selected output device, reset the available output channel
+ // options
+ int indexOfOutput = m_outputDeviceList.indexOf(getOutputDevice());
+ if (indexOfOutput == -1) {
+ std::cerr << "Invalid state. Output device index should never be -1" << std::endl;
+ return;
+ }
+
+ int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput);
+ if (numDevicesChannelsAvailable < 1) {
+ std::cerr << "Invalid state. Number of channels should never be less than 1"
+ << std::endl;
+ return;
+ } else if (numDevicesChannelsAvailable == 1) {
+ // Set the output channels combo to only have channel 1 as an option
+ QJsonObject outputChannelsComboElement = QJsonObject();
+ outputChannelsComboElement.insert(QString::fromStdString("label"),
+ QString::fromStdString("1"));
+ outputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
+ QVariant(0).toInt());
+ outputChannelsComboElement.insert(QString::fromStdString("numChannels"),
+ QVariant(1).toInt());
+ QJsonArray outputChannelsComboModel;
+ outputChannelsComboModel.push_back(outputChannelsComboElement);
+ m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
+
+ // Set the only allowed options for these variables automatically
+ m_parentPtr->setBaseOutputChannel(0);
+ m_parentPtr->setNumOutputChannels(1);
+ } else {
+ // set the output channels selector to have the options based on the currently
+ // selected device
+ QJsonArray outputChannelsComboModel;
+ for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+ if (i % 2 == 0) {
+ QJsonObject element = QJsonObject();
+ element.insert(
+ QString::fromStdString("label"),
+ QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+ element.insert(QString::fromStdString("baseChannel"),
+ QVariant(i).toInt());
+ element.insert(QString::fromStdString("numChannels"),
+ QVariant(2).toInt());
+ outputChannelsComboModel.push_back(element);
+ }
+ }
+ m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
+
+ // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on
+ // this device's option, use the first two channels by default
+ if (getBaseOutputChannel() + getNumOutputChannels()
+ > numDevicesChannelsAvailable) {
+ // we're in the case where numDevicesChannelsAvailable >= 2, so always have
+ // the ability to use the first 2 channels
+ m_parentPtr->setBaseOutputChannel(0);
+ m_parentPtr->setNumOutputChannels(2);
+ }
+ }
+}
+
+#endif // RT_AUDIO
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsAudio.h
+ * \author Matt Horton
+ * \date September 2022
+ */
+
+#ifndef VSDAUDIO_H
+#define VSDAUDIO_H
+
+#include <QJsonArray>
+#include <QList>
+#include <QObject>
+#include <QSharedPointer>
+#include <QString>
+#include <QStringList>
+#include <QTimer>
+
+#include "../AudioInterface.h"
+#include "../jacktrip_globals.h"
+#include "vsPermissions.h"
+
+#ifdef RT_AUDIO
+#include "../RtAudioInterface.h"
+#endif
+
+class Analyzer;
+class JackTrip;
+class Meter;
+class Monitor;
+class QThread;
+class Tone;
+class Volume;
+class VsAudioWorker;
+
+class VsAudio : public QObject
+{
+ Q_OBJECT
+
+ // state shared with QML
+ Q_PROPERTY(bool audioReady READ getAudioReady NOTIFY signalAudioReadyChanged)
+ Q_PROPERTY(bool scanningDevices READ getScanningDevices WRITE setScanningDevices
+ NOTIFY signalScanningDevicesChanged)
+ Q_PROPERTY(bool feedbackDetectionEnabled READ getFeedbackDetectionEnabled WRITE
+ setFeedbackDetectionEnabled NOTIFY feedbackDetectionEnabledChanged)
+ Q_PROPERTY(bool deviceModelsInitialized READ getDeviceModelsInitialized NOTIFY
+ deviceModelsInitializedChanged)
+ Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT)
+ Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY
+ audioBackendChanged)
+ Q_PROPERTY(
+ int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
+ Q_PROPERTY(
+ int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
+ Q_PROPERTY(int queueBuffer READ getQueueBuffer WRITE setQueueBuffer NOTIFY
+ queueBufferChanged)
+ Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels
+ NOTIFY numInputChannelsChanged)
+ Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels
+ NOTIFY numOutputChannelsChanged)
+ Q_PROPERTY(int baseInputChannel READ getBaseInputChannel WRITE setBaseInputChannel
+ NOTIFY baseInputChannelChanged)
+ Q_PROPERTY(int baseOutputChannel READ getBaseOutputChannel WRITE setBaseOutputChannel
+ NOTIFY baseOutputChannelChanged)
+ Q_PROPERTY(int inputMixMode READ getInputMixMode WRITE setInputMixMode NOTIFY
+ inputMixModeChanged)
+ Q_PROPERTY(
+ bool inputMuted READ getInputMuted WRITE setInputMuted NOTIFY updatedInputMuted)
+ Q_PROPERTY(bool inputClipped READ getInputClipped NOTIFY updatedInputClipped)
+ Q_PROPERTY(bool outputClipped READ getOutputClipped NOTIFY updatedOutputClipped)
+ Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY
+ updatedInputVolume)
+ Q_PROPERTY(float outputVolume READ getOutputVolume WRITE setOutputVolume NOTIFY
+ updatedOutputVolume)
+ Q_PROPERTY(float monitorVolume READ getMonitorVolume WRITE setMonitorVolume NOTIFY
+ updatedMonitorVolume)
+ Q_PROPERTY(QString inputDevice READ getInputDevice WRITE setInputDevice NOTIFY
+ inputDeviceChanged)
+ Q_PROPERTY(QString outputDevice READ getOutputDevice WRITE setOutputDevice NOTIFY
+ outputDeviceChanged)
+ Q_PROPERTY(QVector<float> inputMeterLevels READ getInputMeterLevels NOTIFY
+ updatedInputMeterLevels)
+ Q_PROPERTY(QVector<float> outputMeterLevels READ getOutputMeterLevels NOTIFY
+ updatedOutputMeterLevels)
+ Q_PROPERTY(
+ QJsonArray inputComboModel READ getInputComboModel NOTIFY inputComboModelChanged)
+ Q_PROPERTY(QJsonArray outputComboModel READ getOutputComboModel NOTIFY
+ outputComboModelChanged)
+ Q_PROPERTY(QJsonArray inputChannelsComboModel READ getInputChannelsComboModel NOTIFY
+ inputChannelsComboModelChanged)
+ Q_PROPERTY(QJsonArray outputChannelsComboModel READ getOutputChannelsComboModel NOTIFY
+ outputChannelsComboModelChanged)
+ Q_PROPERTY(QJsonArray inputMixModeComboModel READ getInputMixModeComboModel NOTIFY
+ inputMixModeComboModelChanged)
+ Q_PROPERTY(QStringList bufferSizeComboModel READ getBufferSizeComboModel CONSTANT)
+ Q_PROPERTY(QStringList audioBackendComboModel READ getAudioBackendComboModel CONSTANT)
+ Q_PROPERTY(
+ QString devicesWarning READ getDevicesWarningMsg NOTIFY devicesWarningChanged)
+ Q_PROPERTY(QString devicesError READ getDevicesErrorMsg NOTIFY devicesErrorChanged)
+ Q_PROPERTY(QString devicesWarningHelpUrl READ getDevicesWarningHelpUrl NOTIFY
+ devicesWarningHelpUrlChanged)
+ Q_PROPERTY(QString devicesErrorHelpUrl READ getDevicesErrorHelpUrl NOTIFY
+ devicesErrorHelpUrlChanged)
+ Q_PROPERTY(bool highLatencyFlag READ getHighLatencyFlag NOTIFY highLatencyFlagChanged)
+
+ public:
+ enum AudioBackendType {
+ JACK = 0, ///< Jack Mode
+ RTAUDIO ///< RtAudio Mode
+ };
+
+ // Constructor
+ explicit VsAudio(QObject* parent = nullptr);
+ virtual ~VsAudio();
+
+ // allow VirtualStudio to get Permissions to bind to QML view
+ VsPermissions& getPermissions() { return *m_permissionsPtr; }
+ VsAudioWorker& getWorker() { return *m_audioWorkerPtr; }
+
+ // allow VirtualStudio to create new audio interfaces
+ AudioInterface* newAudioInterface(JackTrip* jackTripPtr = nullptr);
+
+ // allow VirtualStudio to load and save settings
+ void loadSettings();
+ void saveSettings();
+
+ // getters for state shared with QML
+ bool backendAvailable() const;
+ bool jackIsAvailable() const;
+ bool asioIsAvailable() const;
+ bool getAudioReady() const { return m_audioReady; }
+ bool getScanningDevices() const { return m_scanningDevices; }
+ bool getFeedbackDetectionEnabled() const { return m_feedbackDetectionEnabled; }
+ bool getDeviceModelsInitialized() const { return m_deviceModelsInitialized; }
+ bool getUseRtAudio() const { return m_backend == AudioBackendType::RTAUDIO; }
+ QString getAudioBackend() const
+ {
+ return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK");
+ }
+ int getSampleRate() const { return m_audioSampleRate; }
+ int getBufferSize() const { return m_audioBufferSize; }
+ int getQueueBuffer() const { return m_queueBuffer; }
+ int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; }
+ int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; }
+ int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; }
+ int getBaseOutputChannel() const { return getUseRtAudio() ? m_baseOutputChannel : 0; }
+ int getInputMixMode() const { return getUseRtAudio() ? m_inputMixMode : 0; }
+ bool getInputMuted() const { return m_inMuted; }
+ bool getInputClipped() const { return m_inputClipped; }
+ bool getOutputClipped() const { return m_outputClipped; }
+ float getInputVolume() const { return m_inMultiplier; }
+ float getOutputVolume() const { return m_outMultiplier; }
+ float getMonitorVolume() const { return m_monMultiplier; }
+ const QString& getInputDevice() const { return m_inputDevice; }
+ const QString& getOutputDevice() const { return m_outputDevice; }
+ const QVector<float>& getInputMeterLevels() const { return m_inputMeterLevels; }
+ const QVector<float>& getOutputMeterLevels() const { return m_outputMeterLevels; }
+ const QJsonArray& getInputComboModel() const { return m_inputComboModel; }
+ const QJsonArray& getOutputComboModel() const { return m_outputComboModel; }
+ const QJsonArray& getInputChannelsComboModel() const
+ {
+ return m_inputChannelsComboModel;
+ }
+ const QJsonArray& getOutputChannelsComboModel() const
+ {
+ return m_outputChannelsComboModel;
+ }
+ const QJsonArray& getInputMixModeComboModel() const
+ {
+ return m_inputMixModeComboModel;
+ }
+ const QStringList& getBufferSizeComboModel() const { return m_bufferSizeComboModel; }
+ const QStringList& getAudioBackendComboModel() const
+ {
+ return m_audioBackendComboModel;
+ }
+ const QString& getDevicesWarningMsg() const { return m_devicesWarningMsg; }
+ const QString& getDevicesErrorMsg() const { return m_devicesErrorMsg; }
+ const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; }
+ const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; }
+ bool getHighLatencyFlag() const { return m_highLatencyFlag; }
+ public slots:
+
+ // setters for state shared with QML
+ void setFeedbackDetectionEnabled(bool enabled);
+ void setAudioBackend(const QString& backend);
+ void setSampleRate(int sampleRate);
+ void setBufferSize(int bufSize);
+ void setQueueBuffer(int queueBuffer);
+ void setNumInputChannels(int numChannels);
+ void setNumOutputChannels(int numChannels);
+ void setBaseInputChannel(int baseChannel);
+ void setBaseOutputChannel(int baseChannel);
+ void setInputMixMode(int mode);
+ void setInputMuted(bool muted);
+ void setInputVolume(float multiplier);
+ void setOutputVolume(float multiplier);
+ void setMonitorVolume(float multiplier);
+ void setInputDevice(const QString& device);
+ void setOutputDevice(const QString& device);
+ void setDevicesErrorMsg(const QString& msg);
+ void setDevicesWarningMsg(const QString& msg);
+ void setDevicesErrorHelpUrl(const QString& url);
+ void setDevicesWarningHelpUrl(const QString& url);
+ void setHighLatencyFlag(bool highLatency);
+
+ // public methods accessible by QML
+ void startAudio(bool block = false);
+ void stopAudio(bool block = false);
+ void refreshDevices(bool block = false);
+ void validateDevices(bool block = false);
+ void restartAudio(bool block = false) { return startAudio(block); }
+ void playOutputAudio() { emit signalPlayOutputAudio(); }
+
+ signals:
+
+ // signals for QML state changes
+ void signalAudioReadyChanged();
+ void signalAudioIsReady();
+ void signalAudioIsNotReady();
+ void signalScanningDevicesChanged();
+ void deviceModelsInitializedChanged(bool initialized);
+ void audioBackendChanged(bool useRtAudio);
+ void sampleRateChanged();
+ void bufferSizeChanged();
+ void queueBufferChanged();
+ void numInputChannelsChanged(int numChannels);
+ void numOutputChannelsChanged(int numChannels);
+ void baseInputChannelChanged(int baseChannel);
+ void baseOutputChannelChanged(int baseChannel);
+ void inputMixModeChanged(int mode);
+ void updatedInputMuted(bool muted);
+ void updatedInputClipped(bool clip);
+ void updatedOutputClipped(bool clip);
+ void updatedInputVolume(float multiplier);
+ void updatedOutputVolume(float multiplier);
+ void updatedMonitorVolume(float multiplier);
+ void inputDeviceChanged(QString device);
+ void outputDeviceChanged(QString device);
+ void updatedInputMeterLevels(const QVector<float>& levels);
+ void updatedOutputMeterLevels(const QVector<float>& levels);
+ void feedbackDetectionEnabledChanged();
+ void feedbackDetected();
+ void inputComboModelChanged();
+ void outputComboModelChanged();
+ void inputChannelsComboModelChanged();
+ void outputChannelsComboModelChanged();
+ void inputMixModeComboModelChanged();
+ void devicesWarningChanged();
+ void devicesErrorChanged();
+ void devicesWarningHelpUrlChanged();
+ void devicesErrorHelpUrlChanged();
+ void highLatencyFlagChanged(bool highLatencyFlag);
+
+ // other signals to perform actions
+ void signalPlayOutputAudio();
+ void signalStartAudio();
+ void signalStopAudio();
+ void signalRefreshDevices();
+ void signalValidateDevices();
+ void signalDevicesValidated();
+
+ private slots:
+ void setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel);
+ void setInputChannelsComboModel(QJsonArray& model);
+ void setOutputChannelsComboModel(QJsonArray& model);
+ void setInputMixModeComboModel(QJsonArray& model);
+
+ private:
+ // private methods
+ void setAudioReady(bool ready);
+ void setScanningDevices(bool b);
+ void detectedFeedbackLoop();
+ void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels);
+ void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels);
+ void appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
+ int numInputChannels, int numOutputChannels);
+ void updateDeviceMessages(AudioInterface& audioInterface);
+ AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr);
+ AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr);
+ void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr);
+
+ // range for volume meters
+ static constexpr float m_meterMax = 0.0;
+ static constexpr float m_meterMin = -64.0;
+
+ // audio bit resolution
+ static constexpr AudioInterface::audioBitResolutionT m_audioBitResolution =
+ AudioInterface::BIT16;
+
+ // state shared with QML
+ AudioBackendType m_backend = AudioBackendType::JACK;
+ bool m_audioReady = false;
+ bool m_scanningDevices = false;
+ bool m_feedbackDetectionEnabled = true;
+ bool m_deviceModelsInitialized = false;
+ int m_audioSampleRate = gDefaultSampleRate;
+ int m_audioBufferSize =
+ gDefaultBufferSizeInSamples; ///< Audio buffer size to process on each callback
+ int m_queueBuffer = 0;
+ int m_numInputChannels = gDefaultNumInChannels;
+ int m_numOutputChannels = gDefaultNumOutChannels;
+ int m_baseInputChannel = 0;
+ int m_baseOutputChannel = 0;
+ int m_inputMixMode = 0;
+ bool m_inMuted = false;
+ bool m_inputClipped = false;
+ bool m_outputClipped = false;
+ float m_inMultiplier = 1.0;
+ float m_outMultiplier = 1.0;
+ float m_monMultiplier = 0;
+
+ QString m_inputDevice;
+ QString m_outputDevice;
+ QVector<float> m_inputMeterLevels;
+ QVector<float> m_outputMeterLevels;
+ QJsonArray m_inputComboModel;
+ QJsonArray m_outputComboModel;
+ QJsonArray m_inputChannelsComboModel;
+ QJsonArray m_outputChannelsComboModel;
+ QJsonArray m_inputMixModeComboModel;
+ QString m_devicesWarningMsg = QStringLiteral("");
+ QString m_devicesErrorMsg = QStringLiteral("");
+ QString m_devicesWarningHelpUrl = QStringLiteral("");
+ QString m_devicesErrorHelpUrl = QStringLiteral("");
+ bool m_highLatencyFlag = false;
+
+ // other state not shared with QML
+ QSharedPointer<VsPermissions> m_permissionsPtr;
+ QScopedPointer<VsAudioWorker> m_audioWorkerPtr;
+ QThread* m_workerThreadPtr;
+ QTimer m_inputClipTimer;
+ QTimer m_outputClipTimer;
+ Meter* m_inputMeterPluginPtr;
+ Meter* m_outputMeterPluginPtr;
+ Volume* m_inputVolumePluginPtr;
+ Volume* m_outputVolumePluginPtr;
+ Monitor* m_monitorPluginPtr;
+ bool mHasErrors; ///< true if one or more error callbacks have been triggered
+
+#ifndef NO_FEEDBACK
+ Analyzer* m_outputAnalyzerPluginPtr;
+#endif
+
+ QStringList m_audioBackendComboModel = {"JACK", "RtAudio"};
+ QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"};
+
+ friend class VsAudioWorker;
+};
+
+/// VsAudioWorker uses a separate thread to help VsAudio
+class VsAudioWorker : public QObject
+{
+ Q_OBJECT
+
+ public:
+ VsAudioWorker(VsAudio* ptr);
+ virtual ~VsAudioWorker() {}
+
+ signals:
+ void signalDevicesValidated();
+ void signalUpdatedDeviceModels(QJsonArray inputComboModel,
+ QJsonArray outputComboModel);
+ void signalError(const QString& errorMessage);
+
+ public slots:
+ void openAudioInterface();
+ void closeAudioInterface();
+#ifdef RT_AUDIO
+ void refreshDevices();
+ void validateDevices();
+
+ private:
+ void updateDeviceModels();
+ void validateInputDevicesState();
+ void validateOutputDevicesState();
+ static void getDeviceList(const QVector<RtAudioDevice>& devices, QStringList& list,
+ QStringList& categories, QList<int>& channels,
+ bool isInput);
+ static QJsonArray formatDeviceList(const QStringList& devices,
+ const QStringList& categories,
+ const QList<int>& channels);
+ QVector<RtAudioDevice> m_devices;
+
+ public:
+ QVector<RtAudioDevice> getDevices() const { return m_devices; }
+#endif
+
+ private:
+ // parent getter wrappers
+ bool getUseRtAudio() const { return m_parentPtr->getUseRtAudio(); }
+ int getNumInputChannels() const { return m_parentPtr->getNumInputChannels(); }
+ int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); }
+ int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); }
+ int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); }
+ int getSampleRate() const { return m_parentPtr->getSampleRate(); }
+ int getBufferSize() const { return m_parentPtr->getBufferSize(); }
+ int getInputMixMode() const { return m_parentPtr->getInputMixMode(); }
+ const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); }
+ const QString& getOutputDevice() const { return m_parentPtr->getOutputDevice(); }
+
+ VsAudio* m_parentPtr;
+ QSharedPointer<AudioInterface> m_audioInterfacePtr;
+ QList<int> m_inputDeviceChannels;
+ QList<int> m_outputDeviceChannels;
+ QStringList m_inputDeviceList;
+ QStringList m_outputDeviceList;
+};
+
+#endif // VSDAUDIO_H
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsAuth.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "vsAuth.h"
+
+#include "./vsConstants.h"
+
+VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api)
+ : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST)
+{
+ m_networkAccessManager = networkAccessManager;
+ m_api = api;
+ m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager));
+
+ connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
+ &VsAuth::initializedCodeFlow);
+ connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowError, this,
+ &VsAuth::handleAuthFailed);
+ connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::onCompletedCodeFlow, this,
+ &VsAuth::codeFlowCompleted);
+ connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this,
+ &VsAuth::codeExpired);
+
+ m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate");
+}
+
+void VsAuth::authenticate(QString currentRefreshToken)
+{
+ if (currentRefreshToken.isEmpty()) {
+ // if no refresh token, initialize device flow
+ m_deviceCodeFlow->grant();
+ } else {
+ m_attemptingRefreshToken = true;
+ emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+
+ // otherwise, use refresh token to gain a new access token
+ m_refreshToken = currentRefreshToken;
+ refreshAccessToken(m_refreshToken);
+ }
+}
+
+void VsAuth::initializedCodeFlow(QString code, QString verificationUrl)
+{
+ m_verificationCode = code;
+ m_verificationUrl = verificationUrl;
+ m_authenticationStage = QStringLiteral("polling");
+
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedVerificationUrl(m_verificationUrl);
+}
+
+void VsAuth::fetchUserInfo(QString accessToken)
+{
+ QNetworkReply* reply = m_api->getAuth0UserInfo();
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "VsAuth::fetchUserInfo Error: "
+ << reply->errorString().toStdString() << std::endl;
+ handleAuthFailed(reply->errorString()); // handle failure
+ emit fetchUserInfoFailed();
+ reply->deleteLater();
+ return;
+ }
+
+ QByteArray response = reply->readAll();
+ QJsonDocument userInfo = QJsonDocument::fromJson(response);
+ QString userId = userInfo.object()[QStringLiteral("sub")].toString();
+
+ reply->deleteLater();
+
+ if (userId.isEmpty()) {
+ std::cout << "VsAuth::fetchUserInfo Error: empty userId" << std::endl;
+ handleAuthFailed("empty userId"); // handle failure
+ emit fetchUserInfoFailed();
+ return;
+ }
+
+ handleAuthSucceeded(userId, accessToken);
+ });
+}
+
+void VsAuth::refreshAccessToken(QString refreshToken)
+{
+ qDebug() << "Refreshing access token";
+ m_authenticationStage = QStringLiteral("refreshing");
+ emit updatedAuthenticationStage(m_authenticationStage);
+
+ QNetworkRequest request = QNetworkRequest(
+ QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
+
+ request.setRawHeader(QByteArray("Content-Type"),
+ QByteArray("application/x-www-form-urlencoded"));
+
+ QString data = QString("grant_type=refresh_token&client_id=%1&refresh_token=%2")
+ .arg(m_clientId, refreshToken);
+
+ // send request
+ QNetworkReply* reply = m_networkAccessManager->post(request, data.toUtf8());
+
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ QByteArray buffer = reply->readAll();
+
+ // Error: failed to get device code
+ QNetworkReply::NetworkError err = reply->error();
+ if (err != QNetworkReply::NoError) {
+ std::cout << "Failed to get new access token: "
+ << reply->errorString().toStdString() << std::endl;
+ handleAuthFailed(reply->errorString()); // handle failure
+ emit refreshTokenFailed();
+ reply->deleteLater();
+ return;
+ }
+
+ // parse JSON from string response
+ QJsonParseError parseError;
+ QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+ if (parseError.error) {
+ std::cout << "Error parsing JSON for Access Token: "
+ << parseError.errorString().toStdString() << std::endl;
+ handleAuthFailed("error parsing access token"); // handle failure
+ emit refreshTokenFailed();
+ reply->deleteLater();
+ return;
+ }
+
+ // received access token
+ QJsonObject object = data.object();
+ QString accessToken = object.value(QLatin1String("access_token")).toString();
+ m_api->setAccessToken(accessToken); // set access token
+ reply->deleteLater();
+ if (m_userId.isEmpty()) {
+ fetchUserInfo(accessToken); // get user ID from Auth0
+ } else {
+ handleRefreshSucceeded(accessToken);
+ }
+ });
+}
+
+void VsAuth::resetCode()
+{
+ if (!m_verificationCode.isEmpty()) {
+ m_deviceCodeFlow->cancelCodeFlow();
+ m_deviceCodeFlow->grant();
+ }
+}
+
+void VsAuth::codeFlowCompleted(QString accessToken, QString refreshToken)
+{
+ m_refreshToken = refreshToken;
+ m_api->setAccessToken(accessToken);
+ fetchUserInfo(accessToken);
+}
+
+void VsAuth::codeExpired()
+{
+ emit deviceCodeExpired();
+}
+
+void VsAuth::handleRefreshSucceeded(QString accessToken)
+{
+ qDebug() << "Successfully refreshed access token";
+
+ m_accessToken = accessToken;
+ m_authenticationStage = QStringLiteral("success");
+ m_errorMessage = QStringLiteral("");
+ m_attemptingRefreshToken = false;
+
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedErrorMessage(m_errorMessage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+}
+
+void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
+{
+ // Success case: we got our access token (either through the refresh token or device
+ // code flow), and fetched the user ID
+ std::cout << "Successfully authenticated Virtual Studio user" << std::endl;
+ std::cout << "User ID: " << userId.toStdString() << std::endl;
+
+ if (m_authenticationStage == QStringLiteral("polling")) {
+ m_authenticationMethod = QStringLiteral("code flow");
+ } else {
+ m_authenticationMethod = QStringLiteral("refresh token");
+ }
+
+ m_userId = userId;
+ m_verificationCode = QStringLiteral("");
+ m_accessToken = accessToken;
+ m_authenticationStage = QStringLiteral("success");
+ m_errorMessage = QStringLiteral("");
+ m_attemptingRefreshToken = false;
+ m_isAuthenticated = true;
+
+ emit updatedUserId(m_userId);
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedErrorMessage(m_errorMessage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedIsAuthenticated(m_isAuthenticated);
+ emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+ emit updatedAuthenticationMethod(m_authenticationMethod);
+
+ // notify UI and virtual studio class of success
+ emit authSucceeded();
+}
+
+void VsAuth::handleAuthFailed(QString errorMessage)
+{
+ // this might get called because there was an error getting the access token,
+ // or there was an issue fetching the user ID. We need both to say
+ // that authentication succeeded
+ std::cout << "Failed to authenticate user" << std::endl;
+
+ m_userId = QStringLiteral("");
+ m_verificationCode = QStringLiteral("");
+ m_accessToken = QStringLiteral("");
+ m_authenticationStage = QStringLiteral("failed");
+ m_errorMessage = errorMessage;
+ m_authenticationMethod = QStringLiteral("");
+ m_attemptingRefreshToken = false;
+ m_isAuthenticated = false;
+
+ emit updatedUserId(m_userId);
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedErrorMessage(m_errorMessage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedIsAuthenticated(m_isAuthenticated);
+ emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+ emit updatedAuthenticationMethod(m_authenticationMethod);
+
+ // notify UI and virtual studio class of failure
+ emit authFailed();
+}
+
+void VsAuth::cancelAuthenticationFlow()
+{
+ qDebug() << "Canceling authentication flow";
+ m_deviceCodeFlow->cancelCodeFlow();
+
+ m_userId = QStringLiteral("");
+ m_verificationCode = QStringLiteral("");
+ m_accessToken = QStringLiteral("");
+ m_authenticationStage = QStringLiteral("unauthenticated");
+ m_errorMessage = QStringLiteral("cancelled");
+ m_isAuthenticated = false;
+
+ emit updatedUserId(m_userId);
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedErrorMessage(m_errorMessage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedIsAuthenticated(m_isAuthenticated);
+}
+
+void VsAuth::logout()
+{
+ if (!m_isAuthenticated) {
+ std::cout << "Warning: attempting to logout while not authenticated" << std::endl;
+ }
+ qDebug() << "Logging out";
+
+ // reset auth state
+ m_userId = QStringLiteral("");
+ m_verificationCode = QStringLiteral("");
+ m_accessToken = QStringLiteral("");
+ m_authenticationStage = QStringLiteral("unauthenticated");
+ m_errorMessage = QStringLiteral("");
+ m_isAuthenticated = false;
+
+ emit updatedUserId(m_userId);
+ emit updatedAuthenticationStage(m_authenticationStage);
+ emit updatedErrorMessage(m_errorMessage);
+ emit updatedVerificationCode(m_verificationCode);
+ emit updatedIsAuthenticated(m_isAuthenticated);
+}
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsAuth.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSAUTH_H
+#define VSAUTH_H
+
+#include <QNetworkAccessManager>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QString>
+#include <iostream>
+
+#include "vsApi.h"
+#include "vsDeviceCodeFlow.h"
+
+class VsAuth : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString authenticationStage READ authenticationStage NOTIFY
+ updatedAuthenticationStage);
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY updatedErrorMessage);
+ Q_PROPERTY(QString verificationCode READ deviceCode NOTIFY updatedVerificationCode);
+ Q_PROPERTY(
+ QString verificationUrl READ deviceVerificationUrl NOTIFY updatedVerificationUrl);
+ Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY updatedIsAuthenticated);
+ Q_PROPERTY(QString authenticationMethod READ authenticationMethod NOTIFY
+ updatedAuthenticationMethod);
+ Q_PROPERTY(bool attemptingRefreshToken READ attemptingRefreshToken NOTIFY
+ updatedAttemptingRefreshToken);
+ Q_PROPERTY(QString userId READ userId NOTIFY updatedUserId);
+ Q_PROPERTY(QString accessToken READ accessToken CONSTANT);
+
+ public:
+ VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api);
+
+ void authenticate(QString currentRefreshToken);
+ void refreshAccessToken(QString refreshToken);
+ Q_INVOKABLE void resetCode();
+ void logout();
+
+ public slots:
+ void cancelAuthenticationFlow();
+
+ // getter methods
+ QString authenticationStage() { return m_authenticationStage; };
+ QString errorMessage() { return m_errorMessage; };
+ QString deviceCode() { return m_verificationCode; };
+ QString deviceVerificationUrl() { return m_verificationUrl; };
+ bool isAuthenticated() { return m_isAuthenticated; };
+ QString userId() { return m_userId; };
+ QString accessToken() { return m_accessToken; };
+ QString refreshToken() { return m_refreshToken; };
+ QString authenticationMethod() { return m_authenticationMethod; }
+ bool attemptingRefreshToken() { return m_attemptingRefreshToken; }
+
+ signals:
+ void updatedAuthenticationStage(QString authenticationStage);
+ void updatedErrorMessage(QString errorMessage);
+ void updatedVerificationCode(QString deviceCode);
+ void updatedVerificationUrl(QUrl verificationUrl);
+ void updatedIsAuthenticated(bool isAuthenticated);
+ void updatedUserId(QString userId);
+ void updatedAuthenticationMethod(QString grant);
+ void updatedAttemptingRefreshToken(bool attemptingRefreshToken);
+ void authSucceeded();
+ void authFailed();
+ void refreshTokenFailed();
+ void fetchUserInfoFailed();
+ void deviceCodeExpired();
+
+ private slots:
+ void handleRefreshSucceeded(QString accessToken);
+ void handleAuthSucceeded(QString userId, QString accessToken);
+ void handleAuthFailed(QString errorMessage);
+ void initializedCodeFlow(QString code, QString verificationUrl);
+ void codeFlowCompleted(QString accessToken, QString refreshToken);
+ void codeExpired();
+
+ private:
+ void fetchUserInfo(QString accessToken);
+
+ QString m_clientId;
+ QString m_authorizationServerHost;
+
+ QString m_authenticationStage = QStringLiteral("unauthenticated");
+ QString m_errorMessage = QStringLiteral("");
+ QString m_verificationCode = QStringLiteral("");
+ QString m_verificationUrl;
+ QString m_authenticationMethod = QStringLiteral("");
+
+ bool m_attemptingRefreshToken = false;
+ bool m_isAuthenticated = false;
+ QString m_userId;
+ QString m_accessToken;
+ QString m_refreshToken;
+
+ QNetworkAccessManager* m_networkAccessManager;
+ VsApi* m_api;
+ QScopedPointer<VsDeviceCodeFlow> m_deviceCodeFlow;
+};
+
+#endif
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsConstants.h
+ * \author Nelson Wang
+ * \date Oct 2022
+ */
+
+#ifndef VSCONSTANTS_H
+#define VSCONSTANTS_H
+
+#include <QString>
+
+const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize");
+const QString AUTH_TOKEN_URI = QStringLiteral("https://auth.jacktrip.org/oauth/token");
+const QString AUTH_AUDIENCE = QStringLiteral("https://api.jacktrip.org");
+const QString AUTH_CLIENT_ID = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I");
+const QString PROD_API_HOST = QStringLiteral("app.jacktrip.com");
+const QString TEST_API_HOST = QStringLiteral("test.jacktrip.com");
+const QString AUTH_SERVER_HOST = QStringLiteral("auth.jacktrip.org");
+
+#endif // VSCONSTANTS_H
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDeeplink.cpp
+ * \author Aaron Wyatt, based on code by Matt Horton
+ * \date February 2023
+ */
+
+#include "vsDeeplink.h"
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QDir>
+#include <QEventLoop>
+#include <QMutexLocker>
+#include <QSettings>
+#include <QTimer>
+
+VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink)
+{
+ setUrlScheme();
+ checkForInstance();
+ QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl");
+}
+
+VsDeeplink::~VsDeeplink()
+{
+ QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip"));
+}
+
+bool VsDeeplink::waitForReady()
+{
+ while (!m_isReady) {
+ QTimer timer;
+ timer.setTimerType(Qt::CoarseTimer);
+ timer.setSingleShot(true);
+
+ QEventLoop loop;
+ QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit);
+ QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ timer.start(100); // wait for 100ms
+ loop.exec();
+ }
+ return m_readyToExit;
+}
+
+void VsDeeplink::readyForSignals()
+{
+ m_readyForSignals = true;
+ if (!m_deeplink.isEmpty()) {
+ emit signalDeeplink(m_deeplink);
+ m_deeplink.clear();
+ }
+}
+
+void VsDeeplink::handleUrl(const QUrl& url)
+{
+ if (m_readyForSignals) {
+ emit signalDeeplink(url);
+ } else {
+ m_deeplink = url;
+ }
+}
+
+void VsDeeplink::checkForInstance()
+{
+ // Create socket
+ m_instanceCheckSocket.reset(new QLocalSocket(this));
+ QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this,
+ &VsDeeplink::connectionReceived, Qt::QueuedConnection);
+ // Create instanceServer to prevent new instances from being created
+ void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError);
+#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
+ errorFunc = &QLocalSocket::error;
+#else
+ errorFunc = &QLocalSocket::errorOccurred;
+#endif
+ QObject::connect(m_instanceCheckSocket.data(), errorFunc, this,
+ &VsDeeplink::connectionFailed);
+ // Check for existing instance
+ m_instanceCheckSocket->connectToServer("jacktripExists");
+}
+
+void VsDeeplink::connectionReceived()
+{
+ // another jacktrip instance is running
+ if (!m_deeplink.isEmpty()) {
+ // pass deeplink to existing instance before quitting
+ QString deeplinkStr = m_deeplink.toString();
+ QByteArray baDeeplink = deeplinkStr.toLocal8Bit();
+ qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink);
+ if (writeBytes < 0) {
+ qDebug() << "sending deeplink failed";
+ } else {
+ qDebug() << "Sent deeplink request to remote instance";
+ }
+
+ // make sure it isn't processed again
+ m_deeplink.clear();
+
+ // End process if another instance exists
+ m_readyToExit = true;
+ }
+
+ m_instanceCheckSocket->waitForBytesWritten();
+ m_instanceCheckSocket->disconnectFromServer(); // remove next
+
+ // let main thread know we are finished
+ m_isReady = true;
+ emit signalIsReady();
+}
+
+void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError)
+{
+ switch (socketError) {
+ case QLocalSocket::ServerNotFoundError:
+ case QLocalSocket::SocketTimeoutError:
+ case QLocalSocket::ConnectionRefusedError:
+ // no other jacktrip instance is running, so we will take over handling deep links
+ qDebug() << "Listening for deep link requests";
+ m_instanceServer.reset(new QLocalServer(this));
+ m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
+ m_instanceServer->listen("jacktripExists");
+ QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
+ &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection);
+ break;
+ case QLocalSocket::PeerClosedError:
+ break;
+ default:
+ qDebug() << m_instanceCheckSocket->errorString();
+ }
+
+ // let main thread know we are finished
+ m_isReady = true;
+ emit signalIsReady();
+}
+
+void VsDeeplink::handleDeeplinkRequest()
+{
+ while (m_instanceServer->hasPendingConnections()) {
+ // Receive URL from 2nd instance
+ QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
+
+ if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
+ qDebug() << "Deeplink socket: never received connection";
+ return;
+ }
+
+ if (!connectedSocket->waitForReadyRead()
+ && connectedSocket->bytesAvailable() <= 0) {
+ qDebug() << "Deeplink socket: not ready and no bytes available: "
+ << connectedSocket->errorString();
+ return;
+ }
+
+ if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
+ qDebug() << "Deeplink socket: ready but no bytes available";
+ break;
+ }
+
+ QByteArray in(connectedSocket->readAll());
+ QString urlString(in);
+ handleUrl(urlString);
+ }
+}
+
+void VsDeeplink::setUrlScheme()
+{
+#ifdef _WIN32
+ // Set url scheme in registry
+ QString path = QDir::toNativeSeparators(qApp->applicationFilePath());
+
+ QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat);
+ set.beginGroup("jacktrip");
+ set.setValue("Default", "URL:JackTrip Protocol");
+ set.setValue("DefaultIcon/Default", path);
+ set.setValue("URL Protocol", "");
+ set.setValue("shell/open/command/Default",
+ QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\"");
+ set.endGroup();
+#endif
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDeeplink.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date August 2023
+ */
+
+#ifndef __VSDEEPLINK_H__
+#define __VSDEEPLINK_H__
+
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QScopedPointer>
+#include <QString>
+#include <QUrl>
+
+class VsDeeplink : public QObject
+{
+ Q_OBJECT
+
+ public:
+ // construct with an instance of the application, to parse command line args
+ VsDeeplink(const QString& deeplink);
+
+ // virtual destructor since it inherits from QObject
+ // this is used to unregister url handler
+ virtual ~VsDeeplink();
+
+ // blocks main thread until local socket server is ready
+ // returns true if a deeplink was handled and we should exit now
+ bool waitForReady();
+
+ // used to let us know VirtualStudio is ready to process deeplink signals
+ void readyForSignals();
+
+ // returns deeplink extracted from command line, if any
+ const QUrl& getDeeplink() const { return m_deeplink; }
+
+ signals:
+
+ // signalIsReady is emitted when the local socket server is ready
+ void signalIsReady();
+
+ // signalDeeplink is emitted when we want the local instance to process a deeplink
+ void signalDeeplink(const QUrl& url);
+
+ private slots:
+
+ // handleUrl is called to trigger processing of a deeplink
+ void handleUrl(const QUrl& url);
+
+ // checks to see if another instance of jacktrip is available to process requests.
+ // if there is, this will send any command line deeplinks to it and exit.
+ // if there isn't, this will start listening for requests.
+ void checkForInstance();
+
+ // called if a connection was established with another instance of VS
+ void connectionReceived();
+
+ // called if unable to connect to another instance of VS
+ void connectionFailed(QLocalSocket::LocalSocketError socketError);
+
+ // called by local socket server to process deeplink requests
+ void handleDeeplinkRequest();
+
+ private:
+ // sets url scheme for windows machines; does nothing on other platforms
+ static void setUrlScheme();
+
+ // used to check if there is a virtual studio instance already running
+ QScopedPointer<QLocalSocket> m_instanceCheckSocket;
+
+ // used to listen for deeplink requests via local socket connections
+ QScopedPointer<QLocalServer> m_instanceServer;
+
+ // used to synchronize with main thread at startup
+ bool m_isReady = false;
+ bool m_readyForSignals = false;
+ bool m_readyToExit = false;
+ QUrl m_deeplink;
+};
+
+#endif // __VSDEEPLINK_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDevice.cpp
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#include "vsDevice.h"
+
+#include <QEventLoop>
+
+// Constructor
+VsDevice::VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
+ QSharedPointer<VsAudio>& audio, QObject* parent)
+ : QObject(parent)
+ , m_auth(auth)
+ , m_api(api)
+ , m_audioConfigPtr(audio)
+ , m_sendVolumeTimer(this)
+{
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ m_apiPrefix = settings.value(QStringLiteral("ApiPrefix"), "").toString();
+ m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString();
+ m_appUUID = settings.value(QStringLiteral("AppUUID"), "").toString();
+ m_appID = settings.value(QStringLiteral("AppID"), "").toString();
+ settings.endGroup();
+
+ if (!m_appID.isEmpty()) {
+ std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
+ }
+
+ m_sendVolumeTimer.setSingleShot(true);
+ connect(&m_sendVolumeTimer, &QTimer::timeout, this, &VsDevice::sendLevels);
+
+ // Set server levels to stored versions
+ sendLevels();
+}
+
+VsDevice::~VsDevice()
+{
+ m_sendVolumeTimer.stop();
+ stopJackTrip(false);
+}
+
+// registerApp idempotently registers an emulated device belonging to the current user
+void VsDevice::registerApp()
+{
+ if (m_appUUID == "") {
+ m_appUUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces);
+ }
+
+ // check if device exists
+ QNetworkReply* reply = m_api->getDevice(m_appID);
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ // Got error
+ if (reply->error() != QNetworkReply::NoError) {
+ QVariant statusCode =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
+ if (!statusCode.isValid()) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ }
+
+ int status = statusCode.toInt();
+ // Device does not exist
+ if (status >= 400 && status < 500) {
+ std::cout << "Device not found. Creating new device." << std::endl;
+
+ if (m_apiPrefix == "" || m_apiSecret == "") {
+ m_apiPrefix = randomString(7);
+ m_apiSecret = randomString(22);
+ }
+
+ registerJTAsDevice();
+ } else {
+ // Other error status. Won't create device.
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ }
+ } else if (m_apiPrefix != "" && m_apiSecret != "") {
+ sendHeartbeat();
+ }
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("AppUUID"), m_appUUID);
+ settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix);
+ settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret);
+ settings.endGroup();
+ if (!m_appID.isEmpty()) {
+ updateState("");
+ }
+
+ reply->deleteLater();
+ });
+}
+
+// removeApp deletes the emulated device
+void VsDevice::removeApp()
+{
+ if (m_appID.isEmpty()) {
+ return;
+ }
+
+ QNetworkReply* reply = m_api->deleteDevice(m_appID);
+ QEventLoop loop;
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ loop.exec();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ } else {
+ m_appID.clear();
+ m_appUUID.clear();
+ m_apiPrefix.clear();
+ m_apiSecret.clear();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.remove(QStringLiteral("AppID"));
+ settings.remove(QStringLiteral("AppUUID"));
+ settings.remove(QStringLiteral("ApiPrefix"));
+ settings.remove(QStringLiteral("ApiSecret"));
+ settings.endGroup();
+ }
+ reply->deleteLater();
+}
+
+// sendHeartbeat is reponsible for sending liveness heartbeats to the API
+void VsDevice::sendHeartbeat()
+{
+ QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
+
+ QJsonObject json = {
+ {QLatin1String("stats_updated_at"), now},
+ {QLatin1String("mac"), m_appUUID},
+ {QLatin1String("version"), QLatin1String(gVersion)},
+ {QLatin1String("type"), "jacktrip_app"},
+ {QLatin1String("apiPrefix"), m_apiPrefix},
+ {QLatin1String("apiSecret"), m_apiSecret},
+ };
+
+ // Add stats to heartbeat body
+ if (!m_pinger.isNull() && m_pinger->active()) {
+ VsPinger::PingStat stats = m_pinger->getPingStats();
+
+ // API server expects RTTs to be in int64 nanoseconds, so we must convert
+ // from milliseconds to nanoseconds
+ int ns_per_ms = 1000000;
+
+ json.insert(QLatin1String("pkts_sent"), (int)stats.packetsSent);
+ json.insert(QLatin1String("pkts_recv"), (int)stats.packetsReceived);
+ json.insert(QLatin1String("min_rtt"), (qint64)(stats.minRtt * ns_per_ms));
+ json.insert(QLatin1String("max_rtt"), (qint64)(stats.maxRtt * ns_per_ms));
+ json.insert(QLatin1String("avg_rtt"), (qint64)(stats.avgRtt * ns_per_ms));
+ 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 = {};
+ pingStats.insert(QLatin1String("packetsSent"), (int)stats.packetsSent);
+ pingStats.insert(QLatin1String("packetsReceived"), (int)stats.packetsReceived);
+ pingStats.insert(QLatin1String("minRtt"), ((int)(10 * stats.minRtt)) / 10.0);
+ pingStats.insert(QLatin1String("maxRtt"), ((int)(10 * stats.maxRtt)) / 10.0);
+ pingStats.insert(QLatin1String("avgRtt"), ((int)(10 * stats.avgRtt)) / 10.0);
+ pingStats.insert(QLatin1String("stdDevRtt"),
+ ((int)(10 * stats.stdDevRtt)) / 10.0);
+ pingStats.insert(QLatin1String("highLatency"),
+ m_audioConfigPtr->getHighLatencyFlag());
+ emit updateNetworkStats(pingStats);
+ }
+
+ QJsonDocument request = QJsonDocument(json);
+
+ if (!m_deviceSocketPtr.isNull() && m_deviceSocketPtr->isValid()) {
+ // Send heartbeat via websocket
+ m_deviceSocketPtr->sendMessage(request.toJson());
+ } else {
+ if (enabled()) {
+ if (m_deviceSocketPtr.isNull()) {
+ qDebug() << "Heartbeat not sent";
+ } else {
+ qDebug() << "Heartbeat not sent; trying to reopen socket";
+ m_deviceSocketPtr->openSocket();
+ }
+ }
+ }
+}
+
+bool VsDevice::hasTerminated()
+{
+ return m_jackTrip.isNull();
+}
+
+// updateState updates the emulated device with the provided state
+void VsDevice::updateState(const QString& serverId)
+{
+ m_deviceAgentConfig.insert("serverId", serverId);
+ m_deviceAgentConfig.insert("enabled", !serverId.isEmpty());
+ QJsonObject json = {
+ {QLatin1String("serverId"), serverId},
+ {QLatin1String("enabled"), !serverId.isEmpty()},
+ };
+ QJsonDocument request = QJsonDocument(json);
+ QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson());
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ }
+ reply->deleteLater();
+ });
+}
+
+void VsDevice::sendLevels()
+{
+ if (m_appID.isEmpty()) {
+ return;
+ }
+ // Add latest volume and mute values to heartbeat body
+ QJsonObject json = {{QLatin1String("version"), QLatin1String(gVersion)},
+ {QLatin1String("captureVolume"),
+ (int)(m_audioConfigPtr->getInputVolume() * 100.0)},
+ {QLatin1String("captureMute"), m_audioConfigPtr->getInputMuted()},
+ {QLatin1String("playbackVolume"),
+ (int)(m_audioConfigPtr->getOutputVolume() * 100.0)},
+ {QLatin1String("playbackMute"), false},
+ {QLatin1String("monitorVolume"),
+ (int)(m_audioConfigPtr->getMonitorVolume() * 100.0)}};
+
+ QJsonDocument request = QJsonDocument(json);
+ QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson());
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ }
+ reply->deleteLater();
+ });
+}
+
+// initJackTrip spawns a new jacktrip process with the desired settings
+JackTrip* VsDevice::initJackTrip(
+ [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input,
+ [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel,
+ [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel,
+ [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode,
+ [[maybe_unused]] int bufferSize, [[maybe_unused]] int queueBuffer,
+ VsServerInfo* studioInfo)
+{
+ m_jackTrip.reset(
+ new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel,
+ numChannelsIn, baseOutputChannel, numChannelsOut,
+ static_cast<AudioInterface::inputMixModeT>(inputMixMode),
+#ifdef WAIR // wair
+ 0,
+#endif // endwhere
+ 4, 1));
+ m_jackTrip->setConnectDefaultAudioPorts(true);
+#ifdef RT_AUDIO
+ if (useRtAudio) {
+ m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
+ m_jackTrip->setAudioBufferSizeInSamples(bufferSize);
+ m_jackTrip->setInputDevice(input);
+ m_jackTrip->setOutputDevice(output);
+ }
+#endif
+ m_jackTrip->setSampleRate(studioInfo->sampleRate());
+ int bindPort = selectBindPort();
+ if (bindPort == 0) {
+ return 0;
+ }
+ m_jackTrip->setBindPorts(bindPort);
+ m_jackTrip->setRemoteClientName(m_appID);
+ m_jackTrip->setBufferStrategy(3); // PLC
+ m_jackTrip->setBufferQueueLength(queueBuffer);
+ m_jackTrip->setUseRtUdpPriority(
+ true); // rt udp priority reduces glitches on desktops
+ m_jackTrip->setPeerAddress(studioInfo->host());
+ m_jackTrip->setPeerPorts(studioInfo->port());
+ m_jackTrip->setPeerHandshakePort(studioInfo->port());
+
+ QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this,
+ &VsDevice::handleJackTripError, Qt::QueuedConnection);
+ QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this,
+ &VsDevice::handleJackTripError, Qt::QueuedConnection);
+
+ return m_jackTrip.data();
+}
+
+// startJackTrip starts the current jacktrip process if applicable
+void VsDevice::startJackTrip(const VsServerInfo& studioInfo)
+{
+ m_stopping = false;
+ m_networkOutage = false;
+ updateState(studioInfo.id());
+
+ // setup websocket listener
+ m_deviceSocketPtr.reset(
+ new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/devices/%2/heartbeat")
+ .arg(m_api->getApiHost(), m_appID)),
+ m_auth->accessToken(), m_apiPrefix, m_apiSecret));
+ connect(m_deviceSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
+ &VsDevice::onTextMessageReceived);
+ connect(m_deviceSocketPtr.get(), &VsWebSocket::disconnected, this,
+ &VsDevice::restartDeviceSocket);
+ m_deviceSocketPtr->openSocket();
+
+ if (!m_jackTrip.isNull()) {
+#ifdef WAIRTOHUB // WAIR
+ m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name
+#else
+ m_jackTrip->startProcess();
+#endif // endwhere
+ }
+
+ // intialize the pinger used to generate network latency statistics for
+ // Virtual Studio
+ QString host = studioInfo.host();
+ if (studioInfo.isManaged()) {
+ host = studioInfo.sessionId();
+ host.append(QString::fromStdString(".jacktrip.cloud"));
+ }
+ if (studioInfo.isManaged() || !studioInfo.sessionId().isEmpty()) {
+ m_pinger.reset(new VsPinger(QString::fromStdString("wss"), host,
+ QString::fromStdString("/ping")));
+ }
+}
+
+// stopJackTrip stops the current jacktrip process if applicable
+void VsDevice::stopJackTrip(bool isReconnecting)
+{
+ // check if another process has already initiated
+ QMutexLocker stopLock(&m_stopMutex);
+ if (m_stopping)
+ return;
+ m_stopping = true;
+
+ // only clear state if we are not reconnecting
+ if (!isReconnecting)
+ updateState("");
+
+ // stop the Virtual Studio pinger
+ if (!m_pinger.isNull()) {
+ m_pinger->stop();
+ m_pinger->unsetToken();
+ }
+
+ if (!m_jackTrip.isNull()) {
+ if (!m_deviceSocketPtr.isNull()) {
+ m_deviceSocketPtr->closeSocket();
+ }
+ m_jackTrip->stop();
+ m_jackTrip.reset();
+ }
+}
+
+// reconcileAgentConfig updates the internal DeviceAgentConfig structure
+void VsDevice::reconcileAgentConfig(QJsonDocument newState)
+{
+ // Only sync if the incoming type matches DeviceAgentConfig:
+ // https://github.com/jacktrip/jacktrip-agent/blob/fd3940c293daf16d8467c62b39a30779d21a0a22/pkg/client/devices.go#L87
+ QJsonObject newObject = newState.object();
+ if (!newObject.contains("enabled")) {
+ return;
+ }
+ for (auto it = newObject.constBegin(); it != newObject.constEnd(); it++) {
+ // if currently enabled but new config is not enabled, disconnect immediately
+ if (enabled() && it.key() == "enabled" && !it.value().toBool()
+ && !m_jackTrip.isNull()) {
+ stopJackTrip(false);
+ }
+ m_deviceAgentConfig.insert(it.key(), it.value());
+ }
+}
+
+// syncDeviceSettings updates volume/mute controls against the API
+void VsDevice::syncDeviceSettings()
+{
+ m_sendVolumeTimer.start(100);
+}
+
+// handleJackTripError is a slot intended to be triggered on jacktrip process signals
+void VsDevice::handleJackTripError()
+{
+ stopJackTrip(false);
+}
+
+// onTextMessageReceived is a slot intended to be triggered by new incoming WSS messages
+void VsDevice::onTextMessageReceived(const QString& message)
+{
+ QJsonDocument newState = QJsonDocument::fromJson(message.toUtf8());
+ QJsonObject newObj = newState.object();
+
+ // We have a heartbeat from which we can read the studio auth token
+ // Use it to set up and start the pinger connection
+ QString token = newState["authToken"].toString();
+ if (!m_pinger.isNull() && !m_pinger->active() && !token.isEmpty()) {
+ m_pinger->setToken(token);
+ m_pinger->start();
+ }
+
+ // capture (input) volume
+ m_audioConfigPtr->setInputVolume(
+ (float)(newObj[QStringLiteral("captureVolume")].toDouble() / 100.0));
+ m_audioConfigPtr->setInputMuted(newObj[QStringLiteral("captureMute")].toBool());
+
+ // playback (output) volume
+ m_audioConfigPtr->setOutputVolume(
+ (float)(newObj[QStringLiteral("playbackVolume")].toDouble() / 100.0));
+
+ // monitor volume
+ m_audioConfigPtr->setMonitorVolume(
+ (float)(newObj[QStringLiteral("monitorVolume")].toDouble() / 100.0));
+
+ reconcileAgentConfig(newState);
+}
+
+void VsDevice::restartDeviceSocket()
+{
+ if (m_deviceAgentConfig[QStringLiteral("serverId")].toString() != "") {
+ if (!m_deviceSocketPtr.isNull()) {
+ m_deviceSocketPtr->openSocket();
+ }
+ }
+}
+
+// registerJTAsDevice creates the emulated device belonging to the current user
+void VsDevice::registerJTAsDevice()
+{
+ /*
+ REGISTER JT APP AS A DEVICE ON VIRTUAL STUDIO
+
+ Defaults:
+ period - 128 - set by studio = buffer size
+ queueBuffer - 0 - set by studio = net queue
+ devicePort - 4464
+ reverb - 0 - off
+ limiter - false
+ compressor - false
+ quality - 2 - high
+ captureMute - false - unused right now
+ captureVolume - 100 - unused right now
+ playbackMute - false - unused right now
+ playbackVolume - 100 - unused right now
+ monitorMute - false - unsure if we should enable
+ monitorVolume - 0 - unsure if we should enable
+ name - "JackTrip App"
+ alsaName - "jacktripapp"
+ overlay - "jacktrip_app"
+ mac - UUID tied to app session
+ version - app version - will need to update in heartbeat
+ apiPrefix - random 7 character string tied to app session
+ apiSecret - random 22 character string tied to app session
+ */
+
+ QJsonObject json = {
+ // TODO: Fix me
+ //{QLatin1String("period"), m_bufferOptions[bufferSize()].toInt()},
+ {QLatin1String("period"), 128},
+ {QLatin1String("queueBuffer"), 0},
+ {QLatin1String("devicePort"), 4464},
+ {QLatin1String("reverb"), 0},
+ {QLatin1String("limiter"), false},
+ {QLatin1String("compressor"), false},
+ {QLatin1String("quality"), 2},
+ {QLatin1String("captureMute"), false},
+ {QLatin1String("captureVolume"), 100},
+ {QLatin1String("playbackMute"), false},
+ {QLatin1String("playbackVolume"), 100},
+ {QLatin1String("monitorMute"), false},
+ {QLatin1String("monitorVolume"), 100},
+ {QLatin1String("alsaName"), "jacktripapp"},
+ {QLatin1String("overlay"), "jacktrip_app"},
+ {QLatin1String("mac"), m_appUUID},
+ {QLatin1String("version"), QLatin1String(gVersion)},
+ {QLatin1String("apiPrefix"), m_apiPrefix},
+ {QLatin1String("apiSecret"), m_apiSecret},
+#if defined(Q_OS_MACOS)
+ {QLatin1String("name"), "JackTrip App (macOS)"},
+#elif defined(Q_OS_WIN)
+ {QLatin1String("name"), "JackTrip App (Windows)"},
+#else
+ {QLatin1String("name"), "JackTrip App"},
+#endif // Q_OS_WIN
+ };
+ QJsonDocument request = QJsonDocument(json);
+
+ QNetworkReply* reply = m_api->postDevice(request.toJson());
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ if (reply->error() != QNetworkReply::NoError) {
+ std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+ reply->deleteLater();
+ return;
+ } else {
+ QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
+ QJsonObject newObject = response.object();
+
+ m_appID = newObject[QStringLiteral("id")].toString();
+
+ // capture (input) volume
+ m_audioConfigPtr->setInputVolume(
+ (float)(newObject[QStringLiteral("captureVolume")].toDouble() / 100.0));
+ m_audioConfigPtr->setInputMuted(
+ newObject[QStringLiteral("captureMute")].toBool());
+
+ // playback (output) volume
+ m_audioConfigPtr->setOutputVolume(
+ (float)(newObject[QStringLiteral("playbackVolume")].toDouble() / 100.0));
+
+ // monitor volume
+ m_audioConfigPtr->setMonitorVolume(
+ (float)(newObject[QStringLiteral("monitorVolume")].toDouble() / 100.0));
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("AppID"), m_appID);
+ settings.endGroup();
+
+ std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
+ sendHeartbeat();
+ }
+
+ reply->deleteLater();
+ });
+}
+
+// enabled returns whether or not the client is connected to a studio
+bool VsDevice::enabled()
+{
+ return m_deviceAgentConfig[QStringLiteral("enabled")].toBool();
+}
+
+// randomString generates a random sequence of characters
+QString VsDevice::randomString(int stringLength)
+{
+ QString str = "";
+ static bool seeded = false;
+ QString allow_symbols(
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
+
+ if (!seeded) {
+ m_randomizer.seed((QTime::currentTime().msec()));
+ seeded = true;
+ }
+
+ for (int i = 0; i < stringLength; ++i) {
+ str.append(allow_symbols.at(m_randomizer.generate() % (allow_symbols.length())));
+ }
+
+ return str;
+}
+
+// selectBindPort finds the next open bind port to use for jacktrip
+int VsDevice::selectBindPort()
+{
+ int candidate = gDefaultPort;
+ if (m_jackTrip.isNull()) {
+ return candidate;
+ }
+ int attempt = 0;
+ while (attempt <= 5000) {
+ candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1);
+ attempt++;
+ if (!m_jackTrip->checkIfPortIsBinded(candidate)) {
+ return candidate;
+ }
+ }
+ return 0;
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDevice.h
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#ifndef VSDEVICE_H
+#define VSDEVICE_H
+
+#include <QMutex>
+#include <QObject>
+#include <QString>
+#include <QTimer>
+#include <QUuid>
+#include <QtWebSockets>
+
+#include "../JackTrip.h"
+#include "../jacktrip_globals.h"
+#include "vsApi.h"
+#include "vsAudio.h"
+#include "vsAuth.h"
+#include "vsConstants.h"
+#include "vsPinger.h"
+#include "vsServerInfo.h"
+#include "vsWebSocket.h"
+
+class VsDevice : public QObject
+{
+ Q_OBJECT
+
+ public:
+ // Constructor
+ explicit VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
+ QSharedPointer<VsAudio>& audio, QObject* parent = nullptr);
+ virtual ~VsDevice();
+
+ // Public functions
+ void registerApp();
+ void removeApp();
+ void sendHeartbeat();
+ bool hasTerminated();
+ JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output,
+ int baseInputChannel, int numChannelsIn, int baseOutputChannel,
+ int numChannelsOut, int inputMixMode, int bufferSize,
+ int queueBuffer, VsServerInfo* studioInfo);
+ 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);
+
+ public slots:
+ void syncDeviceSettings();
+
+ private slots:
+ void handleJackTripError();
+ void onTextMessageReceived(const QString& message);
+ void restartDeviceSocket();
+ void sendLevels();
+
+ private:
+ void updateState(const QString& serverId);
+ void registerJTAsDevice();
+ bool enabled();
+ int selectBindPort();
+ QString randomString(int stringLength);
+
+ QSharedPointer<VsAuth> m_auth;
+ QSharedPointer<VsApi> m_api;
+ QSharedPointer<VsAudio> m_audioConfigPtr;
+ QScopedPointer<VsPinger> m_pinger;
+
+ QString m_appID;
+ QString m_appUUID;
+ QString m_token;
+ QString m_apiPrefix;
+ QString m_apiSecret;
+ QMutex m_stopMutex;
+ QJsonObject m_deviceAgentConfig;
+ QScopedPointer<VsWebSocket> m_deviceSocketPtr;
+ QScopedPointer<JackTrip> m_jackTrip;
+ QRandomGenerator m_randomizer;
+ QTimer m_sendVolumeTimer;
+ bool m_networkOutage = false;
+ bool m_stopping = false;
+};
+
+#endif // VSDEVICE_H
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDeviceCodeFlow.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "./vsDeviceCodeFlow.h"
+
+#include "./vsConstants.h"
+
+VsDeviceCodeFlow::VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager)
+ : m_clientId(AUTH_CLIENT_ID)
+ , m_audience(AUTH_AUDIENCE)
+ , m_authorizationServerHost(AUTH_SERVER_HOST)
+ , m_authenticationError(false)
+ , m_netManager(networkAccessManager)
+{
+ // start polling when the device flow has been initialized
+ connect(this, &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
+ &VsDeviceCodeFlow::startPolling);
+ connect(&m_tokenPollingTimer, &QTimer::timeout, this,
+ &VsDeviceCodeFlow::onPollingTimerTick);
+ connect(&m_deviceFlowExpirationTimer, &QTimer::timeout, this,
+ &VsDeviceCodeFlow::onDeviceCodeExpired);
+
+ m_tokenPollingTimer.setSingleShot(false);
+ m_deviceFlowExpirationTimer.setSingleShot(true);
+}
+
+void VsDeviceCodeFlow::grant()
+{
+ initDeviceAuthorizationCodeFlow();
+}
+
+void VsDeviceCodeFlow::initDeviceAuthorizationCodeFlow()
+{
+ m_authenticationError = false;
+
+ // form initial request for device authorization code
+ QNetworkRequest request = QNetworkRequest(
+ QUrl(QString("https://%1/oauth/device/code").arg(m_authorizationServerHost)));
+
+ request.setRawHeader(QByteArray("Content-Type"),
+ QByteArray("application/x-www-form-urlencoded"));
+
+ QString data =
+ QString("client_id=%1&scope=%2&audience=%3")
+ .arg(m_clientId,
+ QLatin1String("openid profile email offline_access read:servers"),
+ m_audience);
+
+ // send request
+ QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ bool success = processDeviceCodeNetworkReply(reply);
+ if (success) {
+ // notify success along with user code and verification URL
+ emit deviceCodeFlowInitialized(m_userCode, m_verificationUriComplete);
+ } else if (m_authenticationError) {
+ // notify failure
+ emit deviceCodeFlowError(reply->errorString());
+ }
+ reply->deleteLater();
+ });
+}
+
+void VsDeviceCodeFlow::startPolling()
+{
+ if (m_pollingInterval <= 0 || m_deviceCodeValidityDuration <= 0) {
+ std::cout << "Could not start polling. This should not print and indicates a bug."
+ << std::endl;
+ return;
+ }
+
+ // poll on a regular interval, up until the expiration of the code
+ m_tokenPollingTimer.setInterval(m_pollingInterval * 1000);
+ m_deviceFlowExpirationTimer.setInterval(m_deviceCodeValidityDuration * 1000);
+
+ m_tokenPollingTimer.start();
+ m_deviceFlowExpirationTimer.start();
+}
+
+void VsDeviceCodeFlow::stopPolling()
+{
+ if (m_tokenPollingTimer.isActive()) {
+ m_tokenPollingTimer.stop();
+ }
+ if (m_deviceFlowExpirationTimer.isActive()) {
+ m_deviceFlowExpirationTimer.stop();
+ }
+}
+
+void VsDeviceCodeFlow::onPollingTimerTick()
+{
+ // form request to /oauth/token
+ QNetworkRequest request = QNetworkRequest(
+ QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
+
+ request.setRawHeader(QByteArray("Content-Type"),
+ QByteArray("application/x-www-form-urlencoded"));
+
+ QString data =
+ QString("client_id=%1&device_code=%2&grant_type=%3")
+ .arg(m_clientId, m_deviceCode,
+ QLatin1String("urn:ietf:params:oauth:grant-type:device_code"));
+
+ // send send request for token
+ QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
+ connect(reply, &QNetworkReply::finished, this, [=]() {
+ bool success = processPollingOAuthTokenNetworkReply(reply);
+ if (m_authenticationError) {
+ // shouldn't happen
+ emit deviceCodeFlowError(reply->errorString());
+ } else if (success) {
+ // flow successfully completed
+ emit onCompletedCodeFlow(m_accessToken, m_refreshToken);
+ // cleanup
+ stopPolling();
+ cleanupDeviceCodeFlow();
+ }
+ reply->deleteLater();
+ });
+}
+
+void VsDeviceCodeFlow::onDeviceCodeExpired()
+{
+ emit deviceCodeFlowTimedOut();
+
+ std::cout << "Device Code has expired." << std::endl;
+ stopPolling();
+ cleanupDeviceCodeFlow();
+}
+
+void VsDeviceCodeFlow::cancelCodeFlow()
+{
+ stopPolling();
+ cleanupDeviceCodeFlow();
+}
+
+bool VsDeviceCodeFlow::processDeviceCodeNetworkReply(QNetworkReply* reply)
+{
+ QByteArray buffer = reply->readAll();
+
+ // Error: failed to get device code
+ if (reply->error()) {
+ std::cout << "Failed to get device code: " << reply->errorString().toStdString()
+ << std::endl;
+ m_authenticationError = true;
+ return false;
+ }
+
+ // parse JSON from string response
+ QJsonParseError parseError;
+ QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+ if (parseError.error) {
+ std::cout << "Error parsing JSON for Device Code: "
+ << parseError.errorString().toStdString() << std::endl;
+ m_authenticationError = true;
+ return false;
+ }
+
+ // get fields
+ QJsonObject object = data.object();
+ m_deviceCode = object.value(QLatin1String("device_code")).toString();
+ m_userCode = object.value(QLatin1String("user_code")).toString();
+ m_verificationUri = object.value(QLatin1String("verification_uri")).toString();
+ m_verificationUriComplete =
+ object.value(QLatin1String("verification_uri_complete")).toString();
+ m_pollingInterval =
+ object.value(QLatin1String("interval")).toInt(2); // default to 2s
+ m_deviceCodeValidityDuration =
+ object.value(QLatin1String("expires_in")).toInt(900); // default to 900s
+
+ // return true if success
+ return true;
+}
+
+bool VsDeviceCodeFlow::processPollingOAuthTokenNetworkReply(QNetworkReply* reply)
+{
+ QByteArray buffer = reply->readAll();
+
+ // Error: failed to get device code (this is expected)
+ if (reply->error()) {
+ return false;
+ }
+
+ // parse JSON from string response
+ QJsonParseError parseError;
+ QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+ if (parseError.error) {
+ std::cout << "Error parsing JSON for access token: "
+ << parseError.errorString().toStdString() << std::endl;
+ return false;
+ }
+
+ // get fields
+ QJsonObject object = data.object();
+ m_idToken = object.value(QLatin1String("id_token")).toString();
+ m_accessToken = object.value(QLatin1String("access_token")).toString();
+ m_refreshToken = object.value(QLatin1String("refresh_token")).toString();
+ m_authenticationError = false;
+
+ // return true if success
+ return true;
+}
+
+void VsDeviceCodeFlow::cleanupDeviceCodeFlow()
+{
+ m_deviceCode = QStringLiteral("");
+ m_userCode = QStringLiteral("");
+ m_verificationUri = QStringLiteral("https://auth.jacktrip.org/activate");
+ m_verificationUriComplete = QStringLiteral("");
+
+ m_pollingInterval = -1;
+ m_deviceCodeValidityDuration = -1;
+}
+
+QString VsDeviceCodeFlow::accessToken()
+{
+ return m_accessToken;
+}
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsDeviceCodeFlow.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSDEVICECODEFLOW_H
+#define VSDEVICECODEFLOW_H
+
+#include <QEventLoop>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QSettings>
+#include <QString>
+#include <QTimer>
+#include <iostream>
+
+#include "vsDeviceCodeFlow.h"
+
+class VsDeviceCodeFlow : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager);
+ virtual ~VsDeviceCodeFlow() { stopPolling(); }
+
+ void grant();
+ void refreshAccessToken(){};
+ void initDeviceAuthorizationCodeFlow();
+
+ bool processDeviceCodeNetworkReply(QNetworkReply* reply);
+ bool processPollingOAuthTokenNetworkReply(QNetworkReply* reply);
+ void startPolling();
+ void stopPolling();
+ void onPollingTimerTick();
+ void onDeviceCodeExpired();
+ void cancelCodeFlow();
+ void cleanupDeviceCodeFlow();
+
+ bool authenticated();
+ QString accessToken();
+
+ signals:
+ void deviceCodeFlowInitialized(QString code, QString verificationUrl);
+ void deviceCodeFlowError(QString errorMessage);
+ void deviceCodeFlowTimedOut();
+ void onCompletedCodeFlow(QString accessToken, QString refreshToken);
+
+ private:
+ QString m_clientId;
+ QString m_audience;
+ QString m_authorizationServerHost;
+
+ // state used specifically in the device code flow
+ QString m_deviceCode;
+ QString m_userCode;
+ QString m_verificationUri;
+ QString m_verificationUriComplete;
+ int m_pollingInterval = -1; // seconds
+ int m_deviceCodeValidityDuration = -1; // seconds
+
+ QTimer m_tokenPollingTimer;
+ QTimer m_deviceFlowExpirationTimer;
+
+ // authentication state variables
+ bool m_authenticationError;
+ QString m_refreshToken;
+ QString m_accessToken;
+ QString m_idToken;
+
+ QScopedPointer<QNetworkAccessManager> m_netManager;
+};
+
+#endif // VSDEVICECODEFLOW
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsMacPermissions.h
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#ifndef __VSMACPERMISSIONS_H__
+#define __VSMACPERMISSIONS_H__
+
+#include <objc/objc.h>
+
+#include <QDebug>
+#include <QObject>
+#include <QString>
+
+#include "vsPermissions.h"
+
+class VsMacPermissions : public VsPermissions
+{
+ Q_OBJECT
+
+ public:
+ explicit VsMacPermissions();
+
+ bool micPermissionChecked() override;
+ Q_INVOKABLE void getMicPermission() override;
+ Q_INVOKABLE void openSystemPrivacy();
+
+ private:
+ QString m_micPermission = "unknown";
+ bool m_micPermissionChecked = false;
+};
+
+#endif // __VSMACPERMISSIONS_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsMacPermissions.mm
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#include "vsMacPermissions.h"
+#include <Foundation/Foundation.h>
+#include <AVFoundation/AVFoundation.h>
+#include <QDesktopServices>
+#include <QSettings>
+#include <QUrl>
+
+VsMacPermissions::VsMacPermissions()
+{
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ m_micPermissionChecked = settings.value(QStringLiteral("MicPermissionChecked"), false).toBool();
+ settings.endGroup();
+}
+
+bool VsMacPermissions::micPermissionChecked()
+{
+ if (m_micPermissionChecked) {
+ getMicPermission();
+ }
+ return m_micPermissionChecked;
+}
+
+void VsMacPermissions::getMicPermission()
+{
+ if (@available(macOS 10.14, *)) {
+ // Request permission to access.
+ switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+ {
+ case AVAuthorizationStatusAuthorized:
+ {
+ // The user has previously granted access.
+ setMicPermission(QStringLiteral("granted"));
+ break;
+ }
+ case AVAuthorizationStatusNotDetermined:
+ {
+ // The app hasn't yet asked the user for access.
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
+ if (granted) {
+ setMicPermission(QStringLiteral("granted"));
+ } else {
+ setMicPermission(QStringLiteral("denied"));
+ }
+ }];
+ setMicPermission(QStringLiteral("unknown"));
+ break;
+ }
+ case AVAuthorizationStatusDenied:
+ {
+ // The user has previously denied access.
+ setMicPermission(QStringLiteral("denied"));
+ }
+ case AVAuthorizationStatusRestricted:
+ {
+ // The user can't grant access due to restrictions.
+ setMicPermission(QStringLiteral("denied"));
+ }
+ }
+ } else {
+ setMicPermission(QStringLiteral("granted"));
+ }
+}
+
+void VsMacPermissions::openSystemPrivacy()
+{
+ QDesktopServices::openUrl(QUrl("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"));
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPermissions.mm
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#include "vsPermissions.h"
+
+#include <QDesktopServices>
+#include <QSettings>
+#include <QUrl>
+
+QString VsPermissions::micPermission()
+{
+ return m_micPermission;
+}
+
+bool VsPermissions::micPermissionChecked()
+{
+ return m_micPermissionChecked;
+}
+
+void VsPermissions::getMicPermission()
+{
+ setMicPermission("granted");
+}
+
+void VsPermissions::setMicPermission(QString status)
+{
+ m_micPermission = status;
+ m_micPermissionChecked = true;
+ emit micPermissionUpdated();
+
+ QSettings settings;
+ settings.beginGroup(QStringLiteral("VirtualStudio"));
+ settings.setValue(QStringLiteral("MicPermissionChecked"), m_micPermissionChecked);
+ settings.endGroup();
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPermissions.h
+ * \author Matt Horton
+ * \date Nov 2022
+ */
+
+#ifndef __VSPERMISSIONS_H__
+#define __VSPERMISSIONS_H__
+
+#include <QDebug>
+#include <QObject>
+#include <QString>
+
+class VsPermissions : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString micPermission READ micPermission NOTIFY micPermissionUpdated)
+
+ public:
+ VsPermissions() = default; // define here and there
+
+ QString micPermission(); // define here
+ virtual bool micPermissionChecked(); // define here and there
+ Q_INVOKABLE virtual void getMicPermission();
+ void setMicPermission(QString status); // define here
+
+ signals:
+ void micPermissionUpdated(); // leave here
+
+ protected:
+#if __APPLE__
+ QString m_micPermission = "unknown";
+ bool m_micPermissionChecked = false;
+#else
+ QString m_micPermission = "granted";
+ bool m_micPermissionChecked = true;
+#endif
+};
+
+#endif // __VSPERMISSIONS_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPinger.cpp
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#include "vsPing.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+// NOTE: It's better not to use
+// using namespace std;
+// because some functions (like exit()) get confused with QT functions
+
+//*******************************************************************************
+VsPing::VsPing(uint32_t pingNum, uint32_t timeout_msec) : mPingNumber(pingNum)
+{
+ connect(&mTimer, &QTimer::timeout, this, &VsPing::onTimeout);
+
+ mTimer.setTimerType(Qt::PreciseTimer);
+ mTimer.setSingleShot(true);
+ mTimer.setInterval(timeout_msec);
+ mTimer.start();
+}
+
+void VsPing::send()
+{
+ QDateTime now = QDateTime::currentDateTime();
+ mSent = now;
+}
+
+void VsPing::receive()
+{
+ QDateTime now = QDateTime::currentDateTime();
+ if (!mTimedOut) {
+ mTimer.stop();
+ mReceivedReply = true;
+ mReceived = now;
+ }
+}
+
+void VsPing::onTimeout()
+{
+ if (!mReceivedReply) {
+ mTimedOut = true;
+ emit timeout(mPingNumber);
+ }
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPing.h
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#ifndef VSPING_H
+#define VSPING_H
+
+#include <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QtWebSockets>
+#include <stdexcept>
+
+/** \brief A helper class for VsPinger
+ *
+ */
+class VsPing : public QObject
+{
+ Q_OBJECT;
+
+ public:
+ explicit VsPing(uint32_t pingNum, uint32_t timeout_msec);
+ virtual ~VsPing() { mTimer.stop(); }
+ uint32_t pingNumber() { return mPingNumber; }
+
+ QDateTime sentTimestamp() { return mSent; }
+ QDateTime receivedTimestamp() { return mReceived; }
+ bool receivedReply() { return mReceivedReply; }
+ bool timedOut() { return mTimedOut; }
+
+ void send();
+ void receive();
+
+ private:
+ uint32_t mPingNumber;
+ QDateTime mSent;
+ QDateTime mReceived;
+
+ QTimer mTimer;
+ bool mTimedOut = false;
+ bool mReceivedReply = false;
+
+ public slots:
+ void onTimeout();
+
+ signals:
+ void timeout(uint32_t pingNum);
+};
+
+#endif // VSPING_H
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPinger.cpp
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#include "vsPinger.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+// NOTE: It's better not to use
+// using namespace std;
+// because some functions (like exit()) get confused with QT functions
+
+//*******************************************************************************
+VsPinger::VsPinger(QString scheme, QString host, QString path)
+{
+ mURL.setScheme(scheme);
+ mURL.setHost(host);
+ mURL.setPath(path);
+
+ mTimer.setTimerType(Qt::PreciseTimer);
+
+ connect(&mSocket, &QWebSocket::binaryMessageReceived, this,
+ &VsPinger::onReceivePingMessage);
+ connect(&mSocket, &QWebSocket::connected, this, &VsPinger::onConnected);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ connect(&mSocket,
+ QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
+ &VsPinger::onError);
+#else
+ connect(&mSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
+ this, &VsPinger::onError);
+#endif
+ connect(&mTimer, &QTimer::timeout, this, &VsPinger::onPingTimer);
+}
+
+//*******************************************************************************
+void VsPinger::start()
+{
+ // fail to start if no token is supplied
+ if (mToken.toStdString() == "") {
+ std::cout << "Error: auth token is not set" << std::endl;
+ return;
+ }
+
+ mTimer.setInterval(mPingInterval);
+
+ QString authVal = "Bearer ";
+ authVal.append(mToken);
+
+ QNetworkRequest req = QNetworkRequest(QUrl(mURL));
+ req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
+ req.setRawHeader(QByteArray("Connection"), QByteArray("upgrade"));
+ req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
+ mSocket.open(req);
+
+ mStarted = true;
+}
+
+//*******************************************************************************
+void VsPinger::stop()
+{
+ mStarted = false;
+ mError = false;
+ mTimer.stop();
+ mSocket.close(QWebSocketProtocol::CloseCodeNormal, NULL);
+}
+
+//*******************************************************************************
+void VsPinger::setToken(QString token)
+{
+ if (mStarted) {
+ std::cout << "Error: cannot set token while pinger is active." << std::endl;
+ return;
+ }
+
+ mToken = token;
+ mAuthorized = true;
+};
+
+//*******************************************************************************
+void VsPinger::unsetToken()
+{
+ if (mStarted) {
+ std::cout << "Error: cannot unset token while pinger is active." << std::endl;
+ return;
+ }
+
+ mToken = QString();
+ mAuthorized = false;
+}
+
+//*******************************************************************************
+void VsPinger::sendPingMessage(const QByteArray& message)
+{
+ if (mAuthorized && !mError) {
+ mSocket.sendBinaryMessage(message);
+ }
+}
+
+//*******************************************************************************
+void VsPinger::updateStats()
+{
+ PingStat stat;
+ stat.packetsReceived = 0;
+ stat.packetsSent = 0;
+
+ uint32_t count = 0;
+
+ std::vector<uint32_t> vec_expired;
+ std::vector<qint64> vec_rtt;
+ std::map<uint32_t, VsPing*>::reverse_iterator it;
+ for (it = mPings.rbegin(); it != mPings.rend(); ++it) {
+ VsPing* ping = it->second;
+
+ // mark this ping as ready to delete, since it will no longer be used in stats
+ if (count >= mPingNumPerInterval) {
+ vec_expired.push_back(ping->pingNumber());
+ count++;
+ } else if (ping->timedOut() || ping->receivedReply()) {
+ // Only include in statistics pings that have timed out or been received.
+ // All others are pending and are not considered in statistics
+ stat.packetsSent++;
+ if (ping->receivedReply()) {
+ stat.packetsReceived++;
+ }
+
+ QDateTime sent = ping->sentTimestamp();
+ QDateTime received = ping->receivedTimestamp();
+ qint64 diff = sent.msecsTo(received);
+
+ // don't include case where dif = 0 in stats, mark as expired instead
+ if (diff != 0) {
+ vec_rtt.push_back(diff);
+ } else {
+ vec_expired.push_back(ping->pingNumber());
+ }
+
+ count++;
+ }
+ }
+
+ // Deleted pings marked as expired by freeing the Ping object
+ // and clearing the map item
+ for (std::vector<uint32_t>::iterator it_expired = vec_expired.begin();
+ it_expired != vec_expired.end(); it_expired++) {
+ uint32_t expiredPingNum = *it_expired;
+ delete mPings.at(expiredPingNum);
+ mPings.erase(expiredPingNum);
+ }
+
+ // Update RTT stats
+ double min_rtt = 0.0;
+ double max_rtt = 0.0;
+ double avg_rtt = 0.0;
+ double stddev_rtt = 0.0;
+
+ // avoid edge case due to min_rtt and max_rtt being at the numeric limits
+ // when vector size is 0
+ if (vec_rtt.size() == 0) {
+ stat.maxRtt = 0;
+ stat.minRtt = 0;
+ stat.avgRtt = 0;
+ stat.stdDevRtt = 0;
+
+ // Update mStats
+ mStats = stat;
+ return;
+ }
+
+ for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
+ it_rtt++) {
+ double rtt = (double)*it_rtt;
+ if (rtt < min_rtt || min_rtt == 0.0) {
+ min_rtt = rtt;
+ }
+ if (rtt > max_rtt || max_rtt == 0.0) {
+ max_rtt = rtt;
+ }
+
+ avg_rtt += rtt / vec_rtt.size();
+ }
+
+ for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
+ it_rtt++) {
+ double rtt = (double)*it_rtt;
+ stddev_rtt += (rtt - avg_rtt) * (rtt - avg_rtt);
+ }
+ stddev_rtt /= vec_rtt.size();
+ stddev_rtt = sqrt(stddev_rtt);
+
+ stat.maxRtt = max_rtt;
+ stat.minRtt = min_rtt;
+ stat.avgRtt = avg_rtt;
+ stat.stdDevRtt = stddev_rtt;
+
+ // Update mStats
+ mStats = stat;
+ return;
+}
+
+//*******************************************************************************
+VsPinger::PingStat VsPinger::getPingStats()
+{
+ return mStats;
+}
+
+//*******************************************************************************
+void VsPinger::onError(QAbstractSocket::SocketError error)
+{
+ cout << "WebSocket Error: " << error << endl;
+ mError = true;
+ mStarted = false;
+ mTimer.stop();
+}
+
+//*******************************************************************************
+void VsPinger::onConnected()
+{
+ // start the ping timer after the connection is established
+ mTimer.start();
+}
+
+//*******************************************************************************
+void VsPinger::onPingTimer()
+{
+ updateStats();
+
+ QByteArray bytes = QByteArray::number(mPingCount);
+ QDateTime now = QDateTime::currentDateTime();
+ this->sendPingMessage(bytes);
+
+ VsPing* ping = new VsPing(mPingCount, mPingInterval);
+ ping->send();
+ mPings[mPingCount] = ping;
+
+ connect(ping, &VsPing::timeout, this, &VsPinger::onPingTimeout);
+
+ mLastPacketSent = mPingCount;
+ mPingCount++;
+}
+
+//*******************************************************************************
+void VsPinger::onPingTimeout(uint32_t pingNum)
+{
+ std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
+ if (it == mPings.end()) {
+ return;
+ }
+
+ updateStats();
+}
+
+//*******************************************************************************
+void VsPinger::onReceivePingMessage(const QByteArray& message)
+{
+ QDateTime now = QDateTime::currentDateTime();
+ uint32_t pingNum = message.toUInt();
+
+ // locate the appropriate corresponding ping message
+ std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
+ if (it == mPings.end()) {
+ return;
+ }
+
+ VsPing* ping = (*it).second;
+
+ // do not apply to pings that have timed out
+ if (!ping->timedOut()) {
+ // update ping data
+ ping->receive();
+
+ // update vsPinger
+ mHasReceivedPing = true;
+ mLastPacketReceived = pingNum;
+ if (pingNum > mLargestPingNumReceived) {
+ mLargestPingNumReceived = pingNum;
+ }
+ }
+
+ updateStats();
+}
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsPinger.h
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#ifndef VSPINGER_H
+#define VSPINGER_H
+
+#include <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QUrl>
+#include <QtWebSockets>
+#include <stdexcept>
+#include <vector>
+
+#include "vsPing.h"
+
+/** \brief VsPinger for generating latency statistics between
+ * Virtual Studio devices and Virtual Studio Servers
+ *
+ */
+class VsPinger : public QObject
+{
+ Q_OBJECT;
+
+ public:
+ /** \brief The class constructor
+ * \param scheme The protocol scheme for the pinger
+ * \param host The hostname of the server
+ * \param path The path to ping the server on
+ */
+ explicit VsPinger(QString scheme, QString host, QString path);
+ virtual ~VsPinger() { stop(); }
+ void start();
+ void stop();
+ bool active() { return mStarted; };
+ void setToken(QString token);
+ void unsetToken();
+
+ struct PingStat {
+ uint32_t packetsReceived = 0;
+ uint32_t packetsSent = 0;
+ double minRtt = 0.0;
+ double maxRtt = 0.0;
+ double avgRtt = 0.0;
+ double stdDevRtt = 0.0;
+ };
+
+ PingStat getPingStats();
+
+ private:
+ QWebSocket mSocket;
+ QUrl mURL;
+ QString mToken;
+ bool mAuthorized = false;
+ bool mStarted = false;
+ bool mError = false;
+
+ QTimer mTimer;
+ uint32_t mPingCount = 0;
+ const uint32_t mPingNumPerInterval = 5;
+ const uint32_t mPingInterval = 1000;
+ const uint32_t mPingTimeout = 1000;
+
+ std::map<uint32_t, VsPing*> mPings;
+
+ uint32_t mLastPacketSent;
+ uint32_t mLastPacketReceived;
+ uint32_t mLargestPingNumReceived =
+ 0; // is 0 if no ping has been received, otherwise, is the largest ping number
+ // received
+ bool mHasReceivedPing = false; // used for edge case where we have't received a ping
+ // yet (mLargestPingNumReceived = 0)
+
+ PingStat mStats;
+
+ void sendPingMessage(const QByteArray& message);
+ void updateStats();
+
+ private slots:
+ void onError(QAbstractSocket::SocketError error);
+ void onConnected();
+ void onPingTimer();
+ void onPingTimeout(uint32_t pingNum);
+ void onReceivePingMessage(const QByteArray& message);
+};
+
+#endif // VSPINGER_H
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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.
+*/
+//*****************************************************************
+
+#ifndef VSQMLCLIPBOARD_H
+#define VSQMLCLIPBOARD_H
+
+#include <QApplication>
+#include <QClipboard>
+#include <QObject>
+
+class VsQmlClipboard : public QObject
+{
+ Q_OBJECT
+ public:
+ explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent)
+ {
+ clipboard = QApplication::clipboard();
+ }
+
+ Q_INVOKABLE void setText(QString text)
+ {
+ clipboard->setText(text, QClipboard::Clipboard);
+ }
+
+ private:
+ QClipboard* clipboard;
+};
+
+#endif // VSQMLCLIPBOARD_H
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsQuickView.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsQuickView.h"
+
+#include <QDesktopServices>
+#include <iostream>
+
+VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent)
+{
+#ifdef Q_OS_MACOS
+ auto* quit = new QAction("&Quit", this);
+
+ QMenuBar* menuBar = new QMenuBar(nullptr);
+ QMenu* appName = menuBar->addMenu("&JackTrip");
+ appName->addAction(quit);
+
+ connect(quit, &QAction::triggered, this, &VsQuickView::closeWindow);
+#endif
+}
+
+bool VsQuickView::event(QEvent* event)
+{
+ if (event->type() == QEvent::Close || event->type() == QEvent::Quit) {
+ emit windowClose();
+ event->ignore();
+ }
+ return QQuickView::event(event);
+}
+
+void VsQuickView::closeWindow()
+{
+ emit windowClose();
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsQuickView.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSQUICKVIEW_H
+#define VSQUICKVIEW_H
+
+#include <QQuickView>
+#ifdef Q_OS_MACOS
+#include <QAction>
+#include <QMenu>
+#include <QMenuBar>
+#include <QObject>
+#endif
+
+class VsQuickView : public QQuickView
+{
+ Q_OBJECT
+
+ public:
+ VsQuickView(QWindow* parent = nullptr);
+ bool event(QEvent* event) override;
+
+ signals:
+ void windowClose();
+
+ private slots:
+ void closeWindow();
+};
+
+#endif // VSQUICKVIEW_H
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsServerInfo.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsServerInfo.h"
+
+VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {}
+
+VsServerInfo& VsServerInfo::operator=(const VsServerInfo& info)
+{
+ m_section = info.m_section;
+ m_name = info.m_name;
+ m_host = info.m_host;
+ m_port = info.m_port;
+ m_enabled = info.m_enabled;
+ m_owner = info.m_owner;
+ m_admin = info.m_admin;
+ m_isManaged = info.m_isManaged;
+ m_isPublic = info.m_isPublic;
+ m_region = info.m_region;
+ m_period = info.m_period;
+ m_sampleRate = info.m_sampleRate;
+ m_queueBuffer = info.m_queueBuffer;
+ m_bannerURL = info.m_bannerURL;
+ m_id = info.m_id;
+ m_sessionId = info.m_sessionId;
+ m_streamId = info.m_streamId;
+ m_status = info.m_status;
+ m_cloudId = info.m_cloudId;
+ m_inviteKey = info.m_inviteKey;
+ return *this;
+}
+
+VsServerInfo::serverSectionT VsServerInfo::section()
+{
+ return m_section;
+}
+
+QString VsServerInfo::type() const
+{
+ if (m_section == YOUR_STUDIOS) {
+ return QStringLiteral("Your Studios");
+ } else if (m_section == SUBSCRIBED_STUDIOS) {
+ return QStringLiteral("Subscribed Studios");
+ } else {
+ return QStringLiteral("Public Studios");
+ }
+}
+
+void VsServerInfo::setSection(serverSectionT section)
+{
+ m_section = section;
+}
+
+QString VsServerInfo::name() const
+{
+ return m_name;
+}
+
+void VsServerInfo::setName(const QString& name)
+{
+ m_name = name;
+}
+
+QString VsServerInfo::host() const
+{
+ return m_host;
+}
+
+QString VsServerInfo::status() const
+{
+ return m_status;
+}
+
+bool VsServerInfo::canConnect() const
+{
+ return !m_host.isEmpty() && m_status == "Ready";
+}
+
+bool VsServerInfo::canStart() const
+{
+ return m_owner || m_admin;
+}
+
+void VsServerInfo::setHost(const QString& host)
+{
+ m_host = host;
+ emit canConnectChanged();
+}
+
+void VsServerInfo::setStatus(const QString& status)
+{
+ m_status = status;
+ emit canConnectChanged();
+}
+
+quint16 VsServerInfo::port() const
+{
+ return m_port;
+}
+
+void VsServerInfo::setPort(quint16 port)
+{
+ m_port = port;
+}
+
+bool VsServerInfo::enabled() const
+{
+ return m_enabled;
+}
+
+void VsServerInfo::setEnabled(bool enabled)
+{
+ m_enabled = enabled;
+}
+
+bool VsServerInfo::isOwner() const
+{
+ return m_owner;
+}
+
+void VsServerInfo::setIsOwner(bool owner)
+{
+ m_owner = owner;
+}
+
+bool VsServerInfo::isAdmin() const
+{
+ return m_admin;
+}
+
+void VsServerInfo::setIsAdmin(bool admin)
+{
+ m_admin = admin;
+}
+
+bool VsServerInfo::isPublic() const
+{
+ return m_isPublic;
+}
+
+void VsServerInfo::setIsPublic(bool isPublic)
+{
+ m_isPublic = isPublic;
+}
+
+QString VsServerInfo::region() const
+{
+ return m_region;
+}
+
+QString VsServerInfo::flag() const
+{
+ QStringList parts = m_region.split(QStringLiteral("-"));
+ if (parts.count() > 1) {
+ QString countryCode = parts.at(1).toUpper();
+ if (countryCode == QStringLiteral("TF")) {
+ countryCode = QStringLiteral("TW");
+ }
+ return QStringLiteral("flags/%1.svg").arg(countryCode);
+ }
+ // Have a fallback here
+ return QStringLiteral("flags/US.svg");
+}
+
+QString VsServerInfo::location() const
+{
+ return m_region;
+}
+
+void VsServerInfo::setRegion(const QString& region)
+{
+ m_region = region;
+}
+
+bool VsServerInfo::isManaged() const
+{
+ return m_isManaged;
+}
+
+void VsServerInfo::setIsManaged(bool isManaged)
+{
+ m_isManaged = isManaged;
+}
+
+quint16 VsServerInfo::period() const
+{
+ return m_period;
+}
+
+void VsServerInfo::setPeriod(quint16 period)
+{
+ m_period = period;
+}
+
+quint32 VsServerInfo::sampleRate() const
+{
+ return m_sampleRate;
+}
+
+void VsServerInfo::setSampleRate(quint32 sampleRate)
+{
+ m_sampleRate = sampleRate;
+}
+
+quint16 VsServerInfo::queueBuffer() const
+{
+ return m_queueBuffer;
+}
+
+void VsServerInfo::setQueueBuffer(quint16 queueBuffer)
+{
+ m_queueBuffer = queueBuffer;
+}
+
+QString VsServerInfo::bannerURL() const
+{
+ return m_bannerURL;
+}
+
+void VsServerInfo::setBannerURL(const QString& bannerURL)
+{
+ m_bannerURL = bannerURL;
+}
+
+QString VsServerInfo::id() const
+{
+ return m_id;
+}
+
+void VsServerInfo::setId(const QString& id)
+{
+ m_id = id;
+}
+
+QString VsServerInfo::sessionId() const
+{
+ return m_sessionId;
+}
+
+void VsServerInfo::setSessionId(const QString& sessionId)
+{
+ m_sessionId = (sessionId == "undefined") ? "" : sessionId;
+}
+
+QString VsServerInfo::streamId() const
+{
+ return m_streamId;
+}
+
+void VsServerInfo::setStreamId(const QString& streamId)
+{
+ m_streamId = (streamId == "undefined") ? "" : streamId;
+}
+
+QString VsServerInfo::inviteKey() const
+{
+ return m_inviteKey;
+}
+
+void VsServerInfo::setInviteKey(const QString& inviteKey)
+{
+ m_inviteKey = inviteKey;
+}
+
+QString VsServerInfo::cloudId() const
+{
+ return m_cloudId;
+}
+
+void VsServerInfo::setCloudId(const QString& cloudId)
+{
+ m_cloudId = cloudId;
+}
+
+bool VsServerInfo::operator<(const VsServerInfo& other) const
+{
+ if (status() == QStringLiteral("Ready")) {
+ if (other.status() != QStringLiteral("Ready")) {
+ return true;
+ }
+ } else if (other.status() == QStringLiteral("Ready")) {
+ return false;
+ }
+ return name() < other.name();
+}
+
+VsServerInfo::~VsServerInfo() = default;
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsServerInfo.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSSERVERINFO_H
+#define VSSERVERINFO_H
+
+#include <QObject>
+
+class VsServerInfo : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString type READ type CONSTANT)
+ Q_PROPERTY(QString name READ name CONSTANT)
+ // Q_PROPERTY(QString host READ host CONSTANT)
+ Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged)
+ Q_PROPERTY(bool canStart READ canStart CONSTANT)
+ // Q_PROPERTY(quint16 port READ port CONSTANT)
+ Q_PROPERTY(bool isPublic READ isPublic CONSTANT)
+ Q_PROPERTY(QString flag READ flag CONSTANT)
+ Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT)
+ Q_PROPERTY(QString location READ location CONSTANT)
+ Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT)
+ Q_PROPERTY(bool isManaged READ isManaged CONSTANT)
+ Q_PROPERTY(quint16 period READ period CONSTANT)
+ Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT)
+ Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT)
+ Q_PROPERTY(QString sessionId READ sessionId CONSTANT)
+ Q_PROPERTY(QString streamId READ streamId CONSTANT)
+ Q_PROPERTY(QString status READ status CONSTANT)
+ Q_PROPERTY(bool enabled READ enabled CONSTANT)
+ Q_PROPERTY(QString cloudId READ cloudId CONSTANT)
+ Q_PROPERTY(QString id READ id CONSTANT)
+ Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT)
+
+ public:
+ enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS };
+
+ explicit VsServerInfo(QObject* parent = nullptr);
+ VsServerInfo& operator=(const VsServerInfo& info);
+ ~VsServerInfo() override;
+
+ serverSectionT section();
+ QString type() const;
+ void setSection(serverSectionT section);
+ QString name() const;
+ void setName(const QString& name);
+ QString host() const;
+ bool canConnect() const;
+ bool canStart() const;
+ void setHost(const QString& host);
+ quint16 port() const;
+ void setPort(quint16 port);
+ bool enabled() const;
+ void setEnabled(bool enabled);
+ bool isOwner() const;
+ void setIsOwner(bool owner);
+ bool isAdmin() const;
+ void setIsAdmin(bool admin);
+ bool isPublic() const;
+ void setIsPublic(bool isPublic);
+ QString region() const;
+ QString flag() const;
+ QString location() const;
+ void setRegion(const QString& region);
+ bool isManaged() const;
+ void setIsManaged(bool isManageable);
+ quint16 period() const;
+ void setPeriod(quint16 period);
+ quint32 sampleRate() const;
+ void setSampleRate(quint32 sampleRate);
+ quint16 queueBuffer() const;
+ void setQueueBuffer(quint16 queueBuffer);
+ QString bannerURL() const;
+ void setBannerURL(const QString& bannerURL);
+ QString id() const;
+ void setId(const QString& id);
+ QString sessionId() const;
+ void setSessionId(const QString& sessionId);
+ QString streamId() const;
+ void setStreamId(const QString& streamId);
+ QString status() const;
+ void setStatus(const QString& status);
+ QString inviteKey() const;
+ void setInviteKey(const QString& inviteKey);
+ QString cloudId() const;
+ void setCloudId(const QString& cloudId);
+ bool operator<(const VsServerInfo& other) const;
+
+ signals:
+ void canConnectChanged();
+
+ private:
+ serverSectionT m_section = PUBLIC_STUDIOS;
+ QString m_name;
+ QString m_host;
+ quint16 m_port;
+ bool m_enabled;
+ bool m_owner;
+ bool m_admin;
+ bool m_isManaged;
+ bool m_isPublic;
+ QString m_region;
+ quint16 m_period;
+ quint32 m_sampleRate;
+ quint16 m_queueBuffer;
+ QString m_bannerURL;
+ QString m_id;
+ QString m_sessionId;
+ QString m_streamId;
+ QString m_status;
+ QString m_cloudId;
+ QString m_inviteKey;
+
+ /* Remaining JSON fields
+ "loopback": true,
+ "stereo": true,
+ "type": "JackTrip",
+ "size": "c5.large",
+ "mixBranch": "main",
+ "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;",
+ "ownerId": "string",
+ "subStatus": "Active",
+ "createdAt": "2021-09-07T17:15:38Z",
+ "expiresAt": "2021-09-07T17:15:38Z",
+ "updatedAt": "2021-09-07T17:15:38Z"
+ */
+};
+
+#endif // VSSERVERINFO_H
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsWebSocket.cpp
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#include "vsWebSocket.h"
+
+#include <QDebug>
+#include <iostream>
+
+// Constructor
+VsWebSocket::VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
+ QString apiSecret, QObject* parent)
+ : QObject(parent)
+ , m_url(url)
+ , m_token(token)
+ , m_apiPrefix(apiPrefix)
+ , m_apiSecret(apiSecret)
+{
+ m_webSocket.reset(new QWebSocket());
+ connect(m_webSocket.get(), &QWebSocket::disconnected, this,
+ &VsWebSocket::disconnected);
+ connect(m_webSocket.get(),
+ QOverload<const QList<QSslError>&>::of(&QWebSocket::sslErrors), this,
+ &VsWebSocket::onSslErrors);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ connect(m_webSocket.get(),
+ QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
+ &VsWebSocket::onError);
+#else
+ connect(m_webSocket.get(),
+ QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this,
+ &VsWebSocket::onError);
+#endif
+ connect(m_webSocket.get(), &QWebSocket::textMessageReceived, this,
+ &VsWebSocket::textMessageReceived);
+}
+
+VsWebSocket::~VsWebSocket()
+{
+ if (isValid()) {
+ closeSocket();
+ }
+ if (!m_webSocket.isNull()) {
+ m_webSocket->disconnect();
+ m_webSocket.reset();
+ }
+}
+
+void VsWebSocket::openSocket()
+{
+ if (isValid()) {
+ return;
+ }
+
+ QNetworkRequest req = QNetworkRequest(QUrl(m_url));
+ QString authVal = "Bearer ";
+ authVal.append(m_token);
+ req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
+ req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade"));
+ req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
+ req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local"));
+ req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8());
+ req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8());
+
+ if (!m_webSocket.isNull()) {
+ m_webSocket->open(req);
+ qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
+ }
+}
+
+void VsWebSocket::closeSocket()
+{
+ if (!m_webSocket.isNull()
+ && m_webSocket->state() != QAbstractSocket::UnconnectedState) {
+ qDebug() << "Closing websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
+ m_webSocket->abort();
+ }
+}
+
+void VsWebSocket::onError(QAbstractSocket::SocketError error)
+{
+ // RemoteHostClosedError may be expected due to finite connection durations
+ // ConnectionRefusedError may be expected if the server-side endpoint is closed
+ if (error != QAbstractSocket::RemoteHostClosedError) {
+ qDebug() << "Websocket error: " << error;
+ }
+ if (!m_webSocket.isNull()) {
+ m_webSocket->abort();
+ }
+}
+
+void VsWebSocket::onSslErrors(const QList<QSslError>& errors)
+{
+ for (int i = 0; i < errors.size(); ++i) {
+ qDebug() << "SSL error: " << errors.at(i);
+ }
+ if (!m_webSocket.isNull()) {
+ m_webSocket->abort();
+ }
+}
+
+void VsWebSocket::sendMessage(const QByteArray& message)
+{
+ if (isValid()) {
+ m_webSocket->sendBinaryMessage(message);
+ }
+}
+
+bool VsWebSocket::isValid()
+{
+ return !m_webSocket.isNull()
+ && m_webSocket->state() == QAbstractSocket::ConnectedState;
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2024 JackTrip Labs, Inc.
+
+ 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 vsWebSocket.h
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#ifndef VSWEBSOCKET_H
+#define VSWEBSOCKET_H
+
+#include <QList>
+#include <QObject>
+#include <QScopedPointer>
+#include <QSslError>
+#include <QString>
+#include <QUrl>
+#include <QtWebSockets>
+
+class VsWebSocket : public QObject
+{
+ Q_OBJECT
+
+ public:
+ // Constructor
+ explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
+ QString apiSecret, QObject* parent = nullptr);
+ virtual ~VsWebSocket();
+
+ // Public functions
+ void openSocket();
+ void closeSocket();
+ void sendMessage(const QByteArray& message);
+ bool isValid();
+
+ signals:
+ void textMessageReceived(const QString& message);
+ void disconnected();
+
+ private slots:
+ void onError(QAbstractSocket::SocketError error);
+ void onSslErrors(const QList<QSslError>& errors);
+
+ private:
+ QScopedPointer<QWebSocket> m_webSocket;
+ QUrl m_url;
+ QString m_token;
+ QString m_apiPrefix;
+ QString m_apiSecret;
+};
+
+#endif // VSWEBSOCKET_H
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M92-120q-9 0-15.652-4.125Q69.696-128.25 66-135q-4.167-6.6-4.583-14.3Q61-157 66-165l388-670q5-8 11.5-11.5T480-850q8 0 14.5 3.5T506-835l388 670q5 8 4.583 15.7-.416 7.7-4.583 14.3-3.696 6.75-10.348 10.875Q877-120 868-120H92Zm52-60h672L480-760 144-180Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5Zm0-111q12.825 0 21.325-8.625T514-378v-164q0-12.75-8.675-21.375-8.676-8.625-21.5-8.625-12.825 0-21.325 8.625T454-542v164q0 12.75 8.675 21.375 8.676 8.625 21.5 8.625ZM480-470Z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="13.758333mm"
+ height="21.960417mm"
+ viewBox="0 0 13.758333 21.960417"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+ sodipodi:docname="wedge.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:document-units="mm"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:zoom="2.0630341"
+ inkscape:cx="-46.291044"
+ inkscape:cy="47.26049"
+ inkscape:window-width="1920"
+ inkscape:window-height="1007"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs2" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:#0c1424;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+ id="wedge"
+ sodipodi:nodetypes="ccccc" />
+ </g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="13.758333mm"
+ height="21.960417mm"
+ viewBox="0 0 13.758333 21.960417"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
+ sodipodi:docname="wedge_inactive.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:document-units="mm"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:zoom="4.0967594"
+ inkscape:cx="0.36614306"
+ inkscape:cy="41.008022"
+ inkscape:window-width="1312"
+ inkscape:window-height="856"
+ inkscape:window-x="0"
+ inkscape:window-y="38"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs2" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+ id="wedge"
+ sodipodi:nodetypes="ccccc" />
+ </g>
+</svg>
echo Including Qt%QTVERSION% Files\r
for /f "tokens=*" %%a in ('objdump -p jacktrip.exe ^| findstr Qt%QTVERSION%Qml.dll') do set VS=%%a\r
if defined VS (\r
- windeployqt -release --qmldir ..\..\src\gui jacktrip.exe\r
+ echo Including QML files\r
+ windeployqt -release --qmldir ..\..\src\vs jacktrip.exe\r
set WIXDEFINES=%WIXDEFINES% -dvs\r
) else (\r
+ echo Not including QML files\r
windeployqt -release jacktrip.exe\r
)\r
set WIXDEFINES=!WIXDEFINES! -ddynamic -dqt%QTVERSION%\r
link_args += 'Winhttp.lib'
link_args += 'Dnsapi.lib'
link_args += 'Iphlpapi.lib'
+ link_args += 'Qwave.lib'
endif
endif