From 0cd2846c0fdd05613436e00fcf07f213b92d0efa Mon Sep 17 00:00:00 2001 From: =?utf8?q?IOhannes=20m=20zm=C3=B6lnig?= Date: Sun, 4 Dec 2022 22:03:20 +0100 Subject: [PATCH] New upstream version 1.6.7+ds0 --- CMakeLists.txt | 21 +- docs/Build/Linux.md | 4 +- docs/changelog.yml | 20 + jacktrip.pro | 19 +- linux/flatpak/org.jacktrip.JackTrip.Devel.yml | 4 +- .../org.jacktrip.JackTrip.Devel.yml.j2 | 4 +- linux/flatpak/org.jacktrip.JackTrip.yml | 4 +- macos/entitlements.plist | 2 + meson.build | 20 +- releases/edge/mac-manifests.json | 30 ++ releases/edge/win-manifests.json | 30 ++ releases/stable/linux-manifests.json | 10 + releases/stable/mac-manifests.json | 10 + releases/stable/win-manifests.json | 10 + src/AudioInterface.cpp | 94 +++- src/AudioInterface.h | 31 +- src/JackAudioInterface.cpp | 8 +- src/JackAudioInterface.h | 6 +- src/JackTrip.cpp | 39 +- src/JackTrip.h | 21 +- src/JitterBuffer.cpp | 3 +- src/JitterBuffer.h | 3 +- src/Regulator.cpp | 20 +- src/Regulator.h | 50 +- src/RingBuffer.cpp | 12 +- src/RingBuffer.h | 9 +- src/RtAudioInterface.cpp | 465 +++++++++++++----- src/RtAudioInterface.h | 20 +- src/UdpDataProtocol.cpp | 130 +++-- src/UdpDataProtocol.h | 3 +- src/gui/Browse.qml | 35 +- src/gui/Connected.qml | 20 +- src/gui/Failed.qml | 2 +- src/gui/FirstLaunch.qml | 8 +- src/gui/Login.qml | 5 +- src/gui/Meter.qml | 7 +- src/gui/Prompt.svg | 9 + src/gui/Settings.qml | 305 +++++++++--- src/gui/Setup.qml | 345 ++++++++++++- src/gui/Studio.qml | 26 +- src/gui/qjacktrip.cpp | 147 +++++- src/gui/qjacktrip.h | 16 + src/gui/qjacktrip.qrc | 1 + src/gui/qjacktrip.ui | 206 ++++---- src/gui/qjacktrip_novs.qrc | 6 +- src/gui/virtualstudio.cpp | 343 +++++++++---- src/gui/virtualstudio.h | 52 +- src/gui/vs.qml | 14 +- src/gui/vsAudioInterface.cpp | 94 +++- src/gui/vsAudioInterface.h | 9 + src/gui/vsDevice.cpp | 4 +- src/gui/vsMacPermissions.h | 64 +++ src/gui/vsMacPermissions.mm | 104 ++++ src/gui/vsPermissions.cpp | 68 +++ src/gui/vsPermissions.h | 70 +++ src/gui/vuMeter.cpp | 75 +++ src/gui/vuMeter.h | 58 +++ src/jacktrip_globals.h | 2 +- win/jacktrip.wxs | 1 + 59 files changed, 2614 insertions(+), 584 deletions(-) create mode 100644 src/gui/Prompt.svg create mode 100644 src/gui/vsMacPermissions.h create mode 100644 src/gui/vsMacPermissions.mm create mode 100644 src/gui/vsPermissions.cpp create mode 100644 src/gui/vsPermissions.h create mode 100644 src/gui/vuMeter.cpp create mode 100644 src/gui/vuMeter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b33c429..456762b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,15 +25,23 @@ if (nogui) set(noupdater TRUE) endif () +if (psi) + set(novs TRUE) +endif () + if (novs) + add_compile_definitions(NO_VS) set(QRC_FILE "src/gui/qjacktrip_novs.qrc") else () set(QRC_FILE "src/gui/qjacktrip.qrc") endif () +if (noupdater) + add_compile_definitions(NO_UPDATER) +endif () + if (psi) add_compile_definitions(PSI) - set(novs TRUE) if (novs) add_compile_definitions(BUILD_TYPE="psi-borg.org NO_VS binary") else () @@ -57,14 +65,6 @@ else () file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}") endif () -if (novs) - add_compile_definitions(NO_VS) -endif () - -if (noupdater) - add_compile_definitions(NO_UPDATER) -endif () - add_compile_definitions(QT_OPENSOURCE) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") @@ -171,6 +171,8 @@ if (NOT nogui) src/gui/about.cpp src/gui/messageDialog.cpp src/gui/textbuf.cpp + src/gui/vuMeter.cpp + src/Meter.cpp ) if (NOT novs) @@ -185,7 +187,6 @@ if (NOT nogui) src/gui/vsUrlHandler.cpp src/gui/vsWebSocket.cpp src/gui/qjacktrip.qrc - src/Meter.cpp src/Volume.cpp src/Tone.cpp # Need to include this for AUTOMOC to do its thing diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index b722e21..3b2df22 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -16,7 +16,7 @@ Optional: ### Fedora ```sh -dnf install qt5-qtbase-devel qt5-qtnetworkauth-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel +dnf install qt5-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel dnf groupinstall "C Development Tools and Libraries" dnf groupinstall "Development Tools" dnf install "pkgconfig(jack)" alsa-lib-devel git help2man @@ -29,7 +29,7 @@ directory or use QtCreator to compile. ### Ubuntu and Debian/Raspbian ```sh apt install --no-install-recommends build-essential qt5-default autoconf automake libtool make libjack-jackd2-dev git help2man -apt install qjackctl qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev qtdeclarative5-dev qml-module-qtquick-controls +apt install qjackctl qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev libqt5websockets5-dev qtdeclarative5-dev qml-module-qtquick-controls apt install librtaudio-dev # if building with RtAudio ``` diff --git a/docs/changelog.yml b/docs/changelog.yml index ce32d2b..2a29c5c 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,23 @@ +- Version: "1.6.7" + Date: 2022-11-03 + Description: + - (added) volume meters in classic mode + - (added) release-acquire ordering for Regular + - (added) audio driver support article in VS mode + - (added) regulatorthread + - (added) studios page first time UI + - (added) non-asio audio devices can be used on windows + - (updated) dependency list in documentation + - (updated) windows opens jacktrip after install + - (updated) Move to overlapped I/O for Windows Networking + - (updated) New default device behavior in Virtual Studio mode + - (fixed) opening links from Virtual Studio + - (fixed) send capture volume as int + - (fixed) connection issues for servers without reverse dns + - (fixed) flatpak build errors + - (fixed) ventura updater crash + - (fixed) uninitialized delete issue + - (remvoed) extraneous call to readSlotNonBlocking - Version: "1.6.6" Date: 2022-11-01 Description: diff --git a/jacktrip.pro b/jacktrip.pro index 77cb80d..fca5420 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -108,6 +108,9 @@ macx { !nogui { LIBS += -framework Foundation CONFIG += objective_c + !novs { + LIBS += -framework AVFoundation + } } } @@ -239,7 +242,8 @@ HEADERS += src/DataProtocol.h \ HEADERS += src/gui/about.h \ src/gui/messageDialog.h \ src/gui/qjacktrip.h \ - src/gui/textbuf.h + src/gui/textbuf.h \ + src/gui/vuMeter.h !novs { HEADERS += src/gui/virtualstudio.h \ src/gui/vsDevice.h \ @@ -247,6 +251,7 @@ HEADERS += src/DataProtocol.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/vsUrlHandler.h \ @@ -300,7 +305,8 @@ SOURCES += src/DataProtocol.cpp \ SOURCES += src/gui/messageDialog.cpp \ src/gui/qjacktrip.cpp \ src/gui/about.cpp \ - src/gui/textbuf.cpp + src/gui/textbuf.cpp \ + src/gui/vuMeter.cpp !novs { SOURCES += src/gui/virtualstudio.cpp \ src/gui/vsDevice.cpp \ @@ -308,6 +314,7 @@ SOURCES += src/DataProtocol.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/vsUrlHandler.cpp @@ -324,8 +331,14 @@ SOURCES += src/DataProtocol.cpp \ macx { HEADERS += src/gui/NoNap.h OBJECTIVE_SOURCES += src/gui/NoNap.mm + !novs { + HEADERS += src/gui/vsMacPermissions.h + OBJECTIVE_SOURCES += src/gui/vsMacPermissions.mm + } } - FORMS += src/gui/qjacktrip.ui src/gui/about.ui src/gui/messageDialog.ui + FORMS += src/gui/qjacktrip.ui \ + src/gui/about.ui \ + src/gui/messageDialog.ui novs { RESOURCES += src/gui/qjacktrip_novs.qrc } else { diff --git a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml index ef0cb43..45a3c75 100644 --- a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml +++ b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml @@ -1,6 +1,6 @@ app-id: org.jacktrip.JackTrip.Devel runtime: org.kde.Platform -runtime-version: '5.15-21.08' +runtime-version: '5.15-22.08' sdk: org.kde.Sdk command: jacktrip finish-args: @@ -19,7 +19,6 @@ cleanup: - /lib/python3.8 - /share/man modules: - - shared-modules/linux-audio/jack2.json - name: python3-pyyaml buildsystem: simple cleanup: [ "*" ] @@ -47,5 +46,6 @@ modules: - -Dprofile=development sources: - type: git + disable-submodules: true url: https://github.com/jacktrip/jacktrip.git branch: dev diff --git a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 index b2d211c..cc43a83 100644 --- a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 +++ b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 @@ -1,6 +1,6 @@ app-id: org.jacktrip.JackTrip.Devel runtime: org.kde.Platform -runtime-version: '5.15-21.08' +runtime-version: '5.15-22.08' sdk: org.kde.Sdk command: jacktrip finish-args: @@ -19,7 +19,6 @@ cleanup: - /lib/python3.8 - /share/man modules: - - shared-modules/linux-audio/jack2.json - name: python3-pyyaml buildsystem: simple cleanup: [ "*" ] @@ -47,5 +46,6 @@ modules: - -Dprofile=development sources: - type: git + disable-submodules: true url: {{ env['REPO'] }} branch: {{ env['REF'] }} diff --git a/linux/flatpak/org.jacktrip.JackTrip.yml b/linux/flatpak/org.jacktrip.JackTrip.yml index 9bdfcc5..be5c414 100644 --- a/linux/flatpak/org.jacktrip.JackTrip.yml +++ b/linux/flatpak/org.jacktrip.JackTrip.yml @@ -1,6 +1,6 @@ app-id: org.jacktrip.JackTrip runtime: org.kde.Platform -runtime-version: '5.15-21.08' +runtime-version: '5.15-22.08' sdk: org.kde.Sdk command: jacktrip finish-args: @@ -19,7 +19,6 @@ cleanup: - /lib/python3.8 - /share/man modules: - - shared-modules/linux-audio/jack2.json - name: python3-pyyaml buildsystem: simple cleanup: [ "*" ] @@ -45,5 +44,6 @@ modules: buildsystem: meson sources: - type: git + disable-submodules: true url: https://github.com/jacktrip/jacktrip.git branch: main diff --git a/macos/entitlements.plist b/macos/entitlements.plist index 8f4b16d..88233d3 100644 --- a/macos/entitlements.plist +++ b/macos/entitlements.plist @@ -8,5 +8,7 @@ com.apple.security.device.audio-input + com.apple.security.cs.allow-jit + diff --git a/meson.build b/meson.build index 6383039..864f9d6 100644 --- a/meson.build +++ b/meson.build @@ -51,6 +51,7 @@ moc_h = ['src/DataProtocol.h', 'src/Tone.h', 'src/JackTripWorker.h', 'src/PacketHeader.h', + 'src/Regulator.h', 'src/Settings.h', 'src/UdpDataProtocol.h', 'src/UdpHubListener.h', @@ -92,13 +93,15 @@ else 'src/gui/qjacktrip.cpp', 'src/gui/about.cpp', 'src/gui/messageDialog.cpp', - 'src/gui/textbuf.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/textbuf.h', + 'src/gui/vuMeter.h' ] ui_h += [ 'src/gui/qjacktrip.ui', @@ -119,6 +122,7 @@ else 'src/gui/vsQuickView.cpp', 'src/gui/vsWebSocket.cpp', 'src/gui/vsUrlHandler.cpp', + 'src/gui/vsPermissions.cpp', 'src/gui/vsPinger.cpp', 'src/gui/vsPing.cpp' ] @@ -129,12 +133,18 @@ else '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/vsUrlHandler.h', 'src/gui/vsQmlClipboard.h', 'src/JTApplication.h' ] + + if host_machine.system() == 'darwin' + moc_h += ['src/gui/vsMacPermissions.h'] + endif + qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system') qres = ['src/gui/qjacktrip.qrc'] endif @@ -183,6 +193,12 @@ if host_machine.system() == 'darwin' add_languages('objcpp') endif +if host_machine.system() == 'darwin' and get_option('novs') == false + src += ['src/gui/vsMacPermissions.mm'] + apple_av_dep = dependency('appleframeworks', modules : ['avfoundation']) + deps += apple_av_dep +endif + jacktrip = executable('jacktrip', src, prepro_files, include_directories: incdirs, dependencies: deps, c_args: c_defines, cpp_args: defines, install: true ) help2man = find_program('help2man', required: false) diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json index c38c9a5..30905ec 100644 --- a/releases/edge/mac-manifests.json +++ b/releases/edge/mac-manifests.json @@ -1,6 +1,36 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.7-rc.2", + "changelog": "Virtual Studio first time UI, Non-ASIO devices on Windows, Windows can't connect fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.2", + "download": { + "date": "2022-11-29T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.7-rc.2/JackTrip-v1.6.7-rc.2-macOS-x64-installer.pkg", + "downloadSize": 11516784, + "sha256": "4fe2e4dbd67986926b6f738cd4d8a22b801fd3cd50aeebee13d2e1de0310b160" + } + }, + { + "version": "1.6.7-rc.1", + "changelog": "New networking code on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.1", + "download": { + "date": "2022-11-07T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-rc.1-macOS-x64-installer.pkg", + "downloadSize": 11481444, + "sha256": "aa07023d130129684dc8d1e395d04eabea9709db32459e203c4fb7cf9759976e" + } + }, + { + "version": "1.6.6", + "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6", + "download": { + "date": "2022-11-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-macOS-x64-installer.pkg", + "downloadSize": 11481324, + "sha256": "e99149ea9bbfb94a2000cfd3848013e44bb8d23e783198005681c2038bcbd313" + } + }, { "version": "1.6.5-rc.1", "changelog": "Adding volume controls, mute, early Qt6 support, and bug fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.5-rc1", diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json index 9b2f0b0..d1412cc 100644 --- a/releases/edge/win-manifests.json +++ b/releases/edge/win-manifests.json @@ -1,6 +1,36 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.7-rc.2", + "changelog": "Virtual Studio first time UI, Non-ASIO devices on Windows, Windows can't connect fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.2", + "download": { + "date": "2022-11-29T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.7-rc.2/JackTrip-v1.6.7-rc.2-Windows-x64-installer.msi", + "downloadSize": 44531712, + "sha256": "656716e1e665187474bda00c89348a405018a69830557b4722e382187ee367e1" + } + }, + { + "version": "1.6.7-rc.1", + "changelog": "New networking code on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.1", + "download": { + "date": "2022-11-07T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-rc.1-Windows-x64-installer.msi", + "downloadSize": 44412928, + "sha256": "a0485d45c615c435fc4d6b0840d0bf8a74f990001eeacfe26a74d61afbd72dfd" + } + }, + { + "version": "1.6.6", + "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6", + "download": { + "date": "2022-11-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Windows-x64-installer.msi", + "downloadSize": 44408832, + "sha256": "828a1f43254db187a33601cc809551617db83e60a51dad3e992c30270967de0c" + } + }, { "version": "1.6.5-rc.1", "changelog": "Adding volume controls, mute, early Qt6 support, and bug fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.5-rc1", diff --git a/releases/stable/linux-manifests.json b/releases/stable/linux-manifests.json index 9cb7d67..a3e43ac 100644 --- a/releases/stable/linux-manifests.json +++ b/releases/stable/linux-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.6", + "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6", + "download": { + "date": "2022-11-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Linux-x64-binary.zip", + "downloadSize": 29837298, + "sha256": "25b15f3208521530b18e2096de722fefe5318149aa296610f8c5eec147586e0a" + } + }, { "version": "1.6.4", "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4", diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json index febfe6a..4de4ce3 100644 --- a/releases/stable/mac-manifests.json +++ b/releases/stable/mac-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.6", + "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6", + "download": { + "date": "2022-11-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-macOS-x64-installer.pkg", + "downloadSize": 11481324, + "sha256": "e99149ea9bbfb94a2000cfd3848013e44bb8d23e783198005681c2038bcbd313" + } + }, { "version": "1.6.4", "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4", diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json index d5d5454..079d3a4 100644 --- a/releases/stable/win-manifests.json +++ b/releases/stable/win-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.6", + "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6", + "download": { + "date": "2022-11-02T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Windows-x64-installer.msi", + "downloadSize": 44408832, + "sha256": "828a1f43254db187a33601cc809551617db83e60a51dad3e992c30270967de0c" + } + }, { "version": "1.6.4", "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4", diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 8ee34b1..29bb308 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -145,7 +145,7 @@ AudioInterface::~AudioInterface() } //******************************************************************************* -void AudioInterface::setup() +void AudioInterface::setup(bool /*verbose*/) { // Allocate buffer memory to read and write mSizeInBytesPerChannel = getSizeInBytesPerChannel(); @@ -683,12 +683,15 @@ void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin) mProcessPluginsFromNetwork.append(plugin); } -void AudioInterface::initPlugins() +void AudioInterface::initPlugins(bool verbose) { int nPlugins = mProcessPluginsFromNetwork.size() + mProcessPluginsToNetwork.size(); if (nPlugins > 0) { - std::cout << "Initializing Faust plugins (have " << nPlugins - << ") at sampling rate " << mSampleRate << "\n"; + if (verbose) { + std::cout << "Initializing Faust plugins (have " << nPlugins + << ") at sampling rate " << mSampleRate << "\n"; + } + for (ProcessPlugin* plugin : qAsConst(mProcessPluginsFromNetwork)) { plugin->setOutgoingToNetwork(false); plugin->updateNumChannels(mNumInChans, mNumOutChans); @@ -766,3 +769,86 @@ int AudioInterface::getSampleRateFromType(samplingRateT rate_type) return sample_rate; } + +//******************************************************************************* +void AudioInterface::setDevicesWarningMsg(warningMessageT msg) +{ + switch (msg) { + case DEVICE_WARN_LATENCY: + mWarningMsg = + "The selected devices don't support low latency. You can use them, but you " + "will experience audio delay. Make sure you have up to date drivers from the " + "manufacturer!"; +#ifdef _WIN32 + mWarningHelpUrl = "https://help.jacktrip.org/hc/en-us/articles/4409919243155"; +#else + mWarningHelpUrl = ""; +#endif + break; + default: + mWarningMsg = ""; + mWarningHelpUrl = ""; + break; + } + + return; +} + +//******************************************************************************* +void AudioInterface::setDevicesErrorMsg(errorMessageT msg) +{ + mErrorMsg = msg; + switch (msg) { + case DEVICE_ERR_INCOMPATIBLE: + mErrorMsg = + "The two devices you have selected are not compatible. Please select a " + "different pair of devices."; +#ifdef _WIN32 + mErrorHelpUrl = "https://help.jacktrip.org/hc/en-us/articles/4409919243155"; +#else + mErrorHelpUrl = ""; +#endif + break; + case DEVICE_ERR_NO_INPUTS: + mErrorMsg = "JackTrip couldn't find any input devices!"; + mErrorHelpUrl = ""; + break; + case DEVICE_ERR_NO_OUTPUTS: + mErrorMsg = "JackTrip couldn't find any output devices!"; + mErrorHelpUrl = ""; + break; + case DEVICE_ERR_NO_DEVICES: + mErrorMsg = "JackTrip couldn't find any audio devices!"; + mErrorHelpUrl = ""; + break; + default: + mErrorMsg = ""; + mErrorHelpUrl = ""; + break; + } + return; +} + +//******************************************************************************* +std::string AudioInterface::getDevicesWarningMsg() +{ + return mWarningMsg; +} + +//******************************************************************************* +std::string AudioInterface::getDevicesErrorMsg() +{ + return mErrorMsg; +} + +//******************************************************************************* +std::string AudioInterface::getDevicesWarningHelpUrl() +{ + return mWarningHelpUrl; +} + +//******************************************************************************* +std::string AudioInterface::getDevicesErrorHelpUrl() +{ + return mErrorHelpUrl; +} \ No newline at end of file diff --git a/src/AudioInterface.h b/src/AudioInterface.h index ba17539..20967d6 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -76,6 +76,16 @@ class AudioInterface UNDEF ///< Undefined }; + enum warningMessageT { DEVICE_WARN_NONE, DEVICE_WARN_LATENCY }; + + enum errorMessageT { + DEVICE_ERR_NONE, + DEVICE_ERR_INCOMPATIBLE, + DEVICE_ERR_NO_INPUTS, + DEVICE_ERR_NO_OUTPUTS, + DEVICE_ERR_NO_DEVICES + }; + /** \brief The class constructor * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) * \param NumInChans Number of Input Channels @@ -99,14 +109,14 @@ class AudioInterface * Packet Size, Bit Resolution, etc... Sub-classes should also call the parent * method to ensure correct inizialization. */ - virtual void setup(); + virtual void setup(bool verbose = true); /// \brief Tell the audio server that we are ready to roll. The /// process-callback will start running. This runs on its own thread. /// \return 0 on success, otherwise a non-zero error code - virtual int startProcess() const = 0; + virtual int startProcess() = 0; /// \brief Stops the process-callback thread /// \return 0 on success, otherwise a non-zero error code - virtual int stopProcess() const = 0; + virtual int stopProcess() = 0; /** \brief Process callback. Subclass should call this callback after obtaining the in_buffer and out_buffer pointers. * \param in_buffer Array of input audio samplers for each channel. The user @@ -139,7 +149,7 @@ class AudioInterface * Initialize all ProcessPlugin modules. * The audio sampling rate (mSampleRate) must be set at this time. */ - void initPlugins(); + void initPlugins(bool verbose = true); virtual void connectDefaultPorts() = 0; /** \brief Convert a 32bit number (sample_t) into one of the bit resolution * supported (audioBitResolutionT). @@ -213,6 +223,11 @@ class AudioInterface * \return Sample Rate in Hz */ static int getSampleRateFromType(samplingRateT rate_type); + std::string getDevicesWarningMsg(); + std::string getDevicesErrorMsg(); + std::string getDevicesWarningHelpUrl(); + std::string getDevicesErrorHelpUrl(); + //------------------------------------------------------------------ private: @@ -260,8 +275,16 @@ class AudioInterface AudioTester* mAudioTesterP{nullptr}; protected: + void setDevicesWarningMsg(warningMessageT msg); + void setDevicesErrorMsg(errorMessageT msg); + bool mProcessingAudio; ///< Set when processing an audio callback buffer pair const uint32_t MAX_AUDIO_BUFFER_SIZE = 8192; + + std::string mWarningMsg; + std::string mErrorMsg; + std::string mWarningHelpUrl; + std::string mErrorHelpUrl; }; #endif // __AUDIOINTERFACE_H__ diff --git a/src/JackAudioInterface.cpp b/src/JackAudioInterface.cpp index 1df61d3..fd02495 100644 --- a/src/JackAudioInterface.cpp +++ b/src/JackAudioInterface.cpp @@ -117,10 +117,10 @@ JackAudioInterface::JackAudioInterface( JackAudioInterface::~JackAudioInterface() {} //******************************************************************************* -void JackAudioInterface::setup() +void JackAudioInterface::setup(bool verbose) { setupClient(); - AudioInterface::setup(); + AudioInterface::setup(verbose); setProcessCallback(); } @@ -271,7 +271,7 @@ void JackAudioInterface::setProcessCallback() } //******************************************************************************* -int JackAudioInterface::startProcess() const +int JackAudioInterface::startProcess() { // Tell the JACK server that we are ready to roll. Our // process() callback will start running now. @@ -283,7 +283,7 @@ int JackAudioInterface::startProcess() const } //******************************************************************************* -int JackAudioInterface::stopProcess() const +int JackAudioInterface::stopProcess() { QMutexLocker locker(&sJackMutex); int code = (jack_deactivate(mClient)); diff --git a/src/JackAudioInterface.h b/src/JackAudioInterface.h index b85d4ec..7a5501a 100644 --- a/src/JackAudioInterface.h +++ b/src/JackAudioInterface.h @@ -92,16 +92,16 @@ class JackAudioInterface : public AudioInterface virtual ~JackAudioInterface(); /// \brief Setup the client - virtual void setup(); + virtual void setup(bool verbose = true); /** \brief Tell the JACK server that we are ready to roll. The * process-callback will start running. This runs on its own thread. * \return 0 on success, otherwise a non-zero error code */ - virtual int startProcess() const; + virtual int startProcess(); /** \brief Stops the process-callback thread * \return 0 on success, otherwise a non-zero error code */ - virtual int stopProcess() const; + virtual int stopProcess(); /// \brief Connect the default ports, capture to sends, and receives to playback void connectDefaultPorts(); diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index 9d21357..5fa1f5a 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -116,6 +116,8 @@ JackTrip::JackTrip(jacktripModeT JacktripMode, dataProtocolT DataProtocolType, , mStopOnTimeout(false) , mSendRingBuffer(NULL) , mReceiveRingBuffer(NULL) + , mRegulatorThreadPtr(NULL) + , mRegulatorWorkerPtr(NULL) , mReceiverBindPort(receiver_bind_port) , mSenderPeerPort(sender_peer_port) , mSenderBindPort(sender_bind_port) @@ -155,6 +157,8 @@ JackTrip::~JackTrip() delete mDataProtocolReceiver; delete mAudioInterface; delete mPacketHeader; + delete mRegulatorWorkerPtr; + delete mRegulatorThreadPtr; delete mSendRingBuffer; delete mReceiveRingBuffer; } @@ -211,7 +215,7 @@ void JackTrip::setupAudio( if (gVerboseFlag) std::cout << " JackTrip:setupAudio before mAudioInterface->setup" << std::endl; - mAudioInterface->setup(); + mAudioInterface->setup(true); if (gVerboseFlag) std::cout << " JackTrip:setupAudio before mAudioInterface->getSampleRate" << std::endl; @@ -236,7 +240,7 @@ void JackTrip::setupAudio( mAudioInterface->setInputDevice(mInputDeviceName); mAudioInterface->setOutputDevice(mOutputDeviceName); mAudioInterface->setBufferSizeInSamples(mAudioBufferSize); - mAudioInterface->setup(); + mAudioInterface->setup(true); // Setup might have reduced number of channels mNumAudioChansIn = mAudioInterface->getNumInputChannels(); mNumAudioChansOut = mAudioInterface->getNumOutputChannels(); @@ -253,7 +257,7 @@ void JackTrip::setupAudio( mAudioInterface->setInputDevice(mInputDeviceName); mAudioInterface->setOutputDevice(mOutputDeviceName); mAudioInterface->setBufferSizeInSamples(mAudioBufferSize); - mAudioInterface->setup(); + mAudioInterface->setup(true); // Setup might have reduced number of channels mNumAudioChansIn = mAudioInterface->getNumInputChannels(); mNumAudioChansOut = mAudioInterface->getNumOutputChannels(); @@ -473,6 +477,12 @@ void JackTrip::startProcess( ID #endif // endwhere ); + + if (mAudioInterface->getDevicesErrorMsg() != "") { + stop(); + return; + } + // cc redundant with instance creator createHeader(mPacketHeaderType); next line // fixme createHeader(mPacketHeaderType); @@ -593,7 +603,19 @@ void JackTrip::completeConnection() mAudioInterface->appendProcessPluginToNetwork(i); } - mAudioInterface->initPlugins(); // mSampleRate known now, which plugins require + if (mBufferStrategy == 3) { + mRegulatorThreadPtr = new QThread(); + mRegulatorThreadPtr->setObjectName("RegulatorThread"); + Regulator* regulatorPtr = reinterpret_cast(mReceiveRingBuffer); + RegulatorWorker* workerPtr = new RegulatorWorker(regulatorPtr); + workerPtr->moveToThread(mRegulatorThreadPtr); + QObject::connect(this, &JackTrip::signalReceivedNetworkPacket, workerPtr, + &RegulatorWorker::pullPacket, Qt::QueuedConnection); + mRegulatorThreadPtr->start(); + mRegulatorWorkerPtr = workerPtr; + } + + mAudioInterface->initPlugins(true); // mSampleRate known now, which plugins require mAudioInterface->startProcess(); // Tell JACK server we are ready for audio flow now if (mConnectDefaultAudioPorts) { @@ -1052,6 +1074,12 @@ void JackTrip::stop(const QString& errorMessage) mHasShutdown = true; std::cout << "Stopping JackTrip..." << std::endl; + if (mRegulatorThreadPtr != nullptr) { + // Stop the Regulator thread + mRegulatorThreadPtr->quit(); + mRegulatorThreadPtr->wait(); + } + if (mDataProtocolSender != nullptr) { // Stop The Sender mDataProtocolSender->stop(); @@ -1086,6 +1114,9 @@ void JackTrip::waitThreads() { mDataProtocolSender->wait(); mDataProtocolReceiver->wait(); + if (mRegulatorThreadPtr != nullptr) { + mRegulatorThreadPtr->wait(); + } } //******************************************************************************* diff --git a/src/JackTrip.h b/src/JackTrip.h index 99cb23a..9393ffc 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -381,7 +381,7 @@ class JackTrip : public QObject int getReceivePacketSizeInBytes() const; virtual void sendNetworkPacket(const int8_t* ptrToSlot) { - mSendRingBuffer->insertSlotNonBlocking(ptrToSlot, 0, 0); + mSendRingBuffer->insertSlotNonBlocking(ptrToSlot, 0, 0, 0); } virtual void receiveBroadcastPacket(int8_t* ptrToReadSlot) { @@ -390,20 +390,18 @@ class JackTrip : public QObject virtual void receiveNetworkPacket(int8_t* ptrToReadSlot) { mReceiveRingBuffer->readSlotNonBlocking(ptrToReadSlot); + if (mBufferStrategy == 3) { + // trigger next packet using RegulatorThread + emit signalReceivedNetworkPacket(); + } } virtual void readAudioBuffer(int8_t* ptrToReadSlot) { mSendRingBuffer->readSlotBlocking(ptrToReadSlot); } - virtual bool writeAudioBuffer(const int8_t* ptrToSlot, int len, int lostLen) - { - return mReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen); - } - virtual bool writeAudioBufferRegulator(const int8_t* ptrToSlot, int len, int seq, - int lostLen) + virtual bool writeAudioBuffer(const int8_t* ptrToSlot, int len, int lostLen, int seq) { - return mReceiveRingBuffer->insertSlotNonBlockingRegulator(ptrToSlot, len, seq, - lostLen); + return mReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen, seq); } uint32_t getBufferSizeInSamples() const { @@ -586,6 +584,7 @@ class JackTrip : public QObject void signalUdpWaitingTooLong(); void signalQueueLengthChanged(int queueLength); void signalAudioStarted(); + void signalReceivedNetworkPacket(); public: /// \brief Set the AudioInteface object @@ -654,6 +653,10 @@ class JackTrip : public QObject RingBuffer* mSendRingBuffer; /// Pointer for the Receive RingBuffer RingBuffer* mReceiveRingBuffer; + /// thread used to pull packets from Regulator (if mBufferStrategy==3) + QThread* mRegulatorThreadPtr; + /// worker used to pull packets from Regulator (if mBufferStrategy==3) + QObject* mRegulatorWorkerPtr; int mReceiverBindPort; ///< Incoming (receiving) port for local machine int mSenderPeerPort; ///< Incoming (receiving) port for peer machine diff --git a/src/JitterBuffer.cpp b/src/JitterBuffer.cpp index 003a0f9..d37479d 100644 --- a/src/JitterBuffer.cpp +++ b/src/JitterBuffer.cpp @@ -117,7 +117,8 @@ JitterBuffer::JitterBuffer(int buf_samples, int qlen, int sample_rate, int strat } //******************************************************************************* -bool JitterBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen) +bool JitterBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen, + [[maybe_unused]] int seq_num) { if (0 == len) { len = mSlotSize; diff --git a/src/JitterBuffer.h b/src/JitterBuffer.h index c61a02e..7414420 100644 --- a/src/JitterBuffer.h +++ b/src/JitterBuffer.h @@ -48,7 +48,8 @@ class JitterBuffer : public RingBuffer int channels, int bit_res); virtual ~JitterBuffer() {} - virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen); + virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen, + int seq_num); virtual void readSlotNonBlocking(int8_t* ptrToReadSlot); virtual void readBroadcastSlot(int8_t* ptrToReadSlot); diff --git a/src/Regulator.cpp b/src/Regulator.cpp index 56a950a..ea6dc2c 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -156,8 +156,10 @@ Regulator::Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen) if (gVerboseFlag) cout << "mHist = " << mHist << " at " << mFPP << "\n"; mBytes = mFPP * mNumChannels * mBitResolutionMode; - mXfrBuffer = new int8_t[mBytes]; + mPullQueue = new int8_t[mBytes * 2]; + mXfrBuffer = mPullQueue; mPacketCnt = 0; // burg initialization + mNextPacket.store(mPullQueue + mBytes, std::memory_order_release); mFadeUp.resize(mFPP, 0.0); mFadeDown.resize(mFPP, 0.0); for (int i = 0; i < mFPP; i++) { @@ -203,7 +205,7 @@ Regulator::Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen) changeGlobal_3(LostWindowMax); changeGlobal_2(NumSlotsMax); // need hg if running GUI if (m_b_BroadcastQueueLength) { - m_b_ReceiveRingBuffer = new JitterBuffer( + m_b_BroadcastRingBuffer = new JitterBuffer( mFPP, qLen, 48000, 1, m_b_BroadcastQueueLength, mNumChannels, mAudioBitRes); qDebug() << "Broadcast started in Regulator with packet queue of" << m_b_BroadcastQueueLength; @@ -241,7 +243,7 @@ void Regulator::printParams(){ Regulator::~Regulator() { - delete[] mXfrBuffer; + delete[] mPullQueue; delete[] mZeros; delete[] mAssembledPacket; delete pushStat; @@ -252,7 +254,7 @@ Regulator::~Regulator() delete[] slot; }; if (m_b_BroadcastQueueLength) - delete m_b_ReceiveRingBuffer; + delete m_b_BroadcastRingBuffer; } void Regulator::setFPPratio() @@ -357,7 +359,7 @@ void Regulator::pushPacket(const int8_t* buf, int seq_num) }; //******************************************************************************* -void Regulator::pullPacket(int8_t* buf) +void Regulator::pullPacket() { QMutexLocker locker(&mMutex); mSkip = 0; @@ -419,7 +421,13 @@ ZERO_OUTPUT: memcpy(mXfrBuffer, mZeros, mBytes); OUTPUT: - memcpy(buf, mXfrBuffer, mBytes); + // swap positions of mXfrBuffer and mNextPacket + mNextPacket.store(mXfrBuffer, std::memory_order_release); + if (mXfrBuffer == mPullQueue) { + mXfrBuffer = mPullQueue + mBytes; + } else { + mXfrBuffer = mPullQueue; + } }; //******************************************************************************* diff --git a/src/Regulator.h b/src/Regulator.h index 9f66afc..cfe631e 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -46,6 +46,8 @@ #include #include +#include +#include #include "AudioInterface.h" #include "RingBuffer.h" @@ -131,23 +133,27 @@ class Regulator : public RingBuffer // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num)) // instead of // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size)) - virtual bool insertSlotNonBlockingRegulator(const int8_t* ptrToSlot, - [[maybe_unused]] int len, - [[maybe_unused]] int seq_num, int lostLen) + virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen, + int seq_num) { shimFPP(ptrToSlot, len, seq_num); if (m_b_BroadcastQueueLength) - m_b_ReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen); + m_b_BroadcastRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen, + seq_num); return (true); } - void pullPacket(int8_t* buf); + // called by RegulatorWorker after each audio callback, to prep next packet + void pullPacket(); + + virtual void readSlotNonBlocking(int8_t* ptrToReadSlot) + { + ::memcpy(ptrToReadSlot, mNextPacket.load(std::memory_order_acquire), mBytes); + } - virtual void readSlotNonBlocking(int8_t* ptrToReadSlot) { pullPacket(ptrToReadSlot); } virtual void readBroadcastSlot(int8_t* ptrToReadSlot) { - m_b_ReceiveRingBuffer->readSlotNonBlocking(ptrToReadSlot); - m_b_ReceiveRingBuffer->readBroadcastSlot(ptrToReadSlot); + m_b_BroadcastRingBuffer->readBroadcastSlot(ptrToReadSlot); } // virtual QString getStats(uint32_t statCount, uint32_t lostCount); @@ -169,7 +175,9 @@ class Regulator : public RingBuffer BurgAlgorithm ba; int mBytes; int mBytesPeerPacket; + int8_t* mPullQueue; int8_t* mXfrBuffer; + std::atomic mNextPacket; int8_t* mAssembledPacket; int mPacketCnt; sample_t bitsToSample(int ch, int frame); @@ -203,8 +211,30 @@ class Regulator : public RingBuffer void changeGlobal_2(int); void changeGlobal_3(int); void printParams(); - /// Pointer for the Receive RingBuffer - RingBuffer* m_b_ReceiveRingBuffer; + + /// Pointer for the Broadcast RingBuffer + RingBuffer* m_b_BroadcastRingBuffer; int m_b_BroadcastQueueLength; }; + +class RegulatorWorker : public QObject +{ + Q_OBJECT; + + public: + RegulatorWorker(Regulator* rPtr) : mRegulatorPtr(rPtr) {} + virtual ~RegulatorWorker() {} + + public slots: + void pullPacket() + { + if (mRegulatorPtr != nullptr) { + mRegulatorPtr->pullPacket(); + } + } + + private: + Regulator* mRegulatorPtr; +}; + #endif //__REGULATOR_H__ diff --git a/src/RingBuffer.cpp b/src/RingBuffer.cpp index 8e362bb..b2fc0e4 100644 --- a/src/RingBuffer.cpp +++ b/src/RingBuffer.cpp @@ -147,7 +147,8 @@ void RingBuffer::readSlotBlocking(int8_t* ptrToReadSlot) } //******************************************************************************* -bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen) +bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen, + [[maybe_unused]] int seq_num) { if (len != mSlotSize && 0 != len) { // RingBuffer does not support mixed buf sizes @@ -183,15 +184,6 @@ bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int los return true; } -//******************************************************************************* -bool RingBuffer::insertSlotNonBlockingRegulator([[maybe_unused]] const int8_t* ptrToSlot, - [[maybe_unused]] int len, - [[maybe_unused]] int seq_num, - [[maybe_unused]] int lostLen) -{ - return true; -} - //******************************************************************************* void RingBuffer::readSlotNonBlocking(int8_t* ptrToReadSlot) { diff --git a/src/RingBuffer.h b/src/RingBuffer.h index f7415cb..a35f0b2 100644 --- a/src/RingBuffer.h +++ b/src/RingBuffer.h @@ -92,13 +92,8 @@ class RingBuffer /** \brief Same as insertSlotBlocking but non-blocking (asynchronous) * \param ptrToSlot Pointer to slot to insert into the RingBuffer */ - virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen); - - /** \brief Same as insertSlotNonBlocking but seq_num for Regulator - * \param ptrToSlot Pointer to slot to insert into the RingBuffer - */ - virtual bool insertSlotNonBlockingRegulator(const int8_t* ptrToSlot, int len, - int seq_num, int lostLen); + virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen, + int seq_num); /** \brief Same as readSlotBlocking but non-blocking (asynchronous) * \param ptrToReadSlot Pointer to read slot from the RingBuffer diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index 5f89eb5..8bd1217 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -37,6 +37,7 @@ #include "RtAudioInterface.h" +#include #include #include "JackTrip.h" @@ -65,11 +66,13 @@ RtAudioInterface::RtAudioInterface(int NumInChans, int NumOutChans, //******************************************************************************* RtAudioInterface::~RtAudioInterface() { - delete mRtAudio; + if (mRtAudio != NULL) { + delete mRtAudio; + } } //******************************************************************************* -void RtAudioInterface::setup() +void RtAudioInterface::setup(bool verbose) { // Initialize Buffer array to read and write audio and members mNumInChans = getNumInputChannels(); @@ -79,54 +82,93 @@ void RtAudioInterface::setup() cout << "Setting Up RtAudio Interface" << endl; cout << gPrintSeparator << endl; - mRtAudio = new RtAudio; - int deviceId_input; - int deviceId_output; - unsigned int n_devices = mRtAudio->getDeviceCount(); - if (n_devices < 1) { + AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE); + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE); + + int index_in = -1; + int index_out = -1; + std::string api_in; + std::string api_out; + + QStringList all_input_devices; + QStringList all_output_devices; + getDeviceList(&all_input_devices, NULL, true); + getDeviceList(&all_output_devices, NULL, false); + + unsigned int n_devices_input = all_input_devices.size(); + unsigned int n_devices_output = all_output_devices.size(); + unsigned int n_devices_total = n_devices_input + n_devices_output; + + RtAudio* rtAudioIn; + RtAudio* rtAudioOut; + + // unsigned int n_devices = mRtAudio->getDeviceCount(); + if (n_devices_total < 1) { + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NO_DEVICES); cout << "No audio devices found!" << endl; std::exit(0); } else { - deviceId_input = getDeviceID(); - if (deviceId_input < 0) { - auto inName = getInputDevice(); - deviceId_input = getDeviceIdFromName(inName, true); - if (!inName.empty() && (deviceId_input < 0)) { - throw std::runtime_error("Requested input device \"" + inName - + "\" not found."); - } + // Locate the selected input audio device + auto inName = getInputDevice(); + getDeviceInfoFromName(inName, &index_in, &api_in, true); + if (!inName.empty() && (index_in < 0)) { + throw std::runtime_error("Requested input device \"" + inName + + "\" not found."); } - if (deviceId_input < 0) { - cout << "Selecting default INPUT device" << endl; - if (mRtAudio->getCurrentApi() == RtAudio::LINUX_PULSE) { - deviceId_input = getDefaultDevice(true); + rtAudioIn = new RtAudio(RtAudio::getCompiledApiByName(api_in)); + + // The selected input audio device is not available, so select the default device + if (index_in < 0) { + // reset rtAudioIn using the system default + delete rtAudioIn; + rtAudioIn = new RtAudio; + api_in = RtAudio::getApiName(rtAudioIn->getCurrentApi()); + + // Edge case for Linux Pulse Audio + if (rtAudioIn->getCurrentApi() == RtAudio::LINUX_PULSE) { + index_in = getDefaultDeviceForLinuxPulseAudio(true); } else { - deviceId_input = mRtAudio->getDefaultInputDevice(); + index_in = rtAudioIn->getDefaultInputDevice(); } + + cout << "Selected default INPUT device" << endl; + } else { + cout << "Selected INPUT device " << inName << endl; } - deviceId_output = getDeviceID(); - if (deviceId_output < 0) { - auto outName = getOutputDevice(); - deviceId_output = getDeviceIdFromName(outName, false); - if (!outName.empty() && (deviceId_output < 0)) { - throw std::runtime_error("Requested output device \"" + outName - + "\" not found."); - } + // Locate the selected output audio device + auto outName = getOutputDevice(); + getDeviceInfoFromName(outName, &index_out, &api_out, false); + if (!outName.empty() && (index_out < 0)) { + throw std::runtime_error("Requested output device \"" + outName + + "\" not found."); } - if (deviceId_output < 0) { - cout << "Selecting default OUTPUT device" << endl; - if (mRtAudio->getCurrentApi() == RtAudio::LINUX_PULSE) { - deviceId_output = getDefaultDevice(false); + rtAudioOut = new RtAudio(RtAudio::getCompiledApiByName(api_out)); + + // The selected output audio device is not available, so select the default device + if (index_out < 0) { + // reset rtAudioIn using the system default + delete rtAudioOut; + rtAudioOut = new RtAudio; + api_out = RtAudio::getApiName(rtAudioOut->getCurrentApi()); + + // Edge case for Linux Pulse Audio + if (rtAudioOut->getCurrentApi() == RtAudio::LINUX_PULSE) { + index_out = getDefaultDeviceForLinuxPulseAudio(false); } else { - deviceId_output = mRtAudio->getDefaultOutputDevice(); + index_out = rtAudioOut->getDefaultOutputDevice(); } + + cout << "Selected default OUTPUT device" << endl; + + } else { + cout << "Selected OUTPUT device " << outName << endl; } } - auto dev_info_input = mRtAudio->getDeviceInfo(deviceId_input); - auto dev_info_output = mRtAudio->getDeviceInfo(deviceId_output); + auto dev_info_input = rtAudioIn->getDeviceInfo(index_in); + auto dev_info_output = rtAudioOut->getDeviceInfo(index_out); if (static_cast(getNumInputChannels()) > dev_info_input.inputChannels) { setNumInputChannels(dev_info_input.inputChannels); @@ -136,16 +178,42 @@ void RtAudioInterface::setup() setNumOutputChannels(dev_info_output.outputChannels); } - cout << "INPUT DEVICE:" << endl; - printDeviceInfo(deviceId_input); - cout << gPrintSeparator << endl; - cout << "OUTPUT DEVICE:" << endl; - printDeviceInfo(deviceId_output); - cout << gPrintSeparator << endl; + if (verbose) { + cout << "INPUT DEVICE:" << endl; + printDeviceInfo(api_in, index_in); + + cout << gPrintSeparator << endl; + cout << "OUTPUT DEVICE:" << endl; + + printDeviceInfo(api_out, index_out); + cout << gPrintSeparator << endl; + } + + if (n_devices_input == 0) { + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NO_INPUTS); + } else if (n_devices_output == 0) { + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NO_OUTPUTS); + } + + delete rtAudioIn; + delete rtAudioOut; + if (api_in == api_out) { + mRtAudio = new RtAudio(RtAudio::getCompiledApiByName(api_in)); +#ifdef _WIN32 + if (api_in != "asio") { + AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_LATENCY); + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE); + } +#endif + } else { + AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE); + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_INCOMPATIBLE); + mRtAudio = NULL; + } RtAudio::StreamParameters in_params, out_params; - in_params.deviceId = deviceId_input; - out_params.deviceId = deviceId_output; + in_params.deviceId = index_in; + out_params.deviceId = index_out; in_params.nChannels = getNumInputChannels(); out_params.nChannels = getNumOutputChannels(); @@ -163,72 +231,46 @@ void RtAudioInterface::setup() unsigned int bufferFrames = getBufferSizeInSamples(); // mBufferSize; try { - // IMPORTANT NOTE: It's VERY important to remember to pass this - // as the user data in the process callback, otherwise member won't + // IMPORTANT NOTE: It's VERY important to remember to pass "this" + // to the user data in the process callback, otherwise member won't // be accessible - mRtAudio->openStream(&out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, - &bufferFrames, &RtAudioInterface::wrapperRtAudioCallback, - this, &options, &RtAudioInterface::RtAudioErrorCallback); + if (mRtAudio != NULL) { + mRtAudio->openStream(&out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, + &bufferFrames, &RtAudioInterface::wrapperRtAudioCallback, + this, &options, &RtAudioInterface::RtAudioErrorCallback); + } + setBufferSize(bufferFrames); } catch (RtAudioError& e) { - std::cout << '\n' << e.getMessage() << '\n' << std::endl; + std::cout << e.getMessage() << '\n' << std::endl; throw std::runtime_error(e.getMessage()); } // Setup parent class - AudioInterface::setup(); -} - -//******************************************************************************* -void RtAudioInterface::listAllInterfaces() -{ - RtAudio rtaudio; - if (rtaudio.getDeviceCount() < 1) { - cout << "No audio devices found!" << endl; - } else { - for (unsigned int i = 0; i < rtaudio.getDeviceCount(); i++) { - printDeviceInfo(i); - cout << gPrintSeparator << endl; - } - } + AudioInterface::setup(verbose); } //******************************************************************************* void RtAudioInterface::printDevices() { - // TODO: evenntually list devices for all RtAudio-compiled backends - RtAudio audio; - audio.showWarnings(false); - cout << "Available audio devices: " << endl; - unsigned int devices = audio.getDeviceCount(); - RtAudio::DeviceInfo info; - for (unsigned int i = 0; i < devices; i++) { - info = audio.getDeviceInfo(i); - if (info.probed == true) { - std::cout << i << ": \"" << info.name << "\" "; - std::cout << "(" << info.inputChannels << " ins, " << info.outputChannels - << " outs)" << endl; - } - } -} - -//******************************************************************************* -int RtAudioInterface::getDeviceIdFromName(std::string deviceName, bool isInput) -{ - RtAudio rtaudio; - for (unsigned int i = 0; i < rtaudio.getDeviceCount(); i++) { - auto info = rtaudio.getDeviceInfo(i); - if (info.probed == true) { - if (info.name == deviceName) { - if (isInput && info.inputChannels > 0) { - return i; - } else if (!isInput && info.outputChannels > 0) { - return i; - } + std::vector apis; + RtAudio::getCompiledApi(apis); + + for (uint32_t i = 0; i < apis.size(); i++) { + RtAudio rtaudio(apis.at(i)); + unsigned int devices = rtaudio.getDeviceCount(); + for (unsigned int j = 0; j < devices; j++) { + RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(j); + if (info.probed == true) { + std::cout << "[" << RtAudio::getApiDisplayName(rtaudio.getCurrentApi()) + << " - " << j << "]" + << ": \""; + std::cout << info.name << "\" "; + std::cout << "(" << info.inputChannels << " ins, " << info.outputChannels + << " outs)" << endl; } } } - return -1; } //******************************************************************************* @@ -238,7 +280,7 @@ int RtAudioInterface::getDeviceIdFromName(std::string deviceName, bool isInput) // Once this functinoality is provided upstream and in the distributions' // package managers, the following function can be removed and the default device // can be obtained by calls to getDefaultInputDevice() / getDefaultOutputDevice() -unsigned int RtAudioInterface::getDefaultDevice(bool isInput) +unsigned int RtAudioInterface::getDefaultDeviceForLinuxPulseAudio(bool isInput) { RtAudio rtaudio; for (unsigned int i = 0; i < rtaudio.getDeviceCount(); i++) { @@ -257,20 +299,19 @@ unsigned int RtAudioInterface::getDefaultDevice(bool isInput) } //******************************************************************************* -void RtAudioInterface::printDeviceInfo(unsigned int deviceId) +void RtAudioInterface::printDeviceInfo(std::string api, unsigned int deviceIndex) { - RtAudio rtaudio; - RtAudio::DeviceInfo info; - int i = deviceId; - info = rtaudio.getDeviceInfo(i); - std::vector sampleRates; - cout << "Audio Device [" << i << "] : " << info.name << endl; + RtAudio rtaudio(RtAudio::getCompiledApiByName(api)); + RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(deviceIndex); + std::vector sampleRates = info.sampleRates; + + cout << "Audio Device [" << RtAudio::getApiDisplayName(rtaudio.getCurrentApi()) + << " - " << deviceIndex << "] : " << info.name << endl; cout << " Output Channels : " << info.outputChannels << endl; cout << " Input Channels : " << info.inputChannels << endl; - sampleRates = info.sampleRates; cout << " Supported Sampling Rates: "; - for (unsigned int ii = 0; ii < sampleRates.size(); ii++) { - cout << sampleRates[ii] << " "; + for (unsigned int i = 0; i < sampleRates.size(); i++) { + cout << sampleRates[i] << " "; } cout << endl; if (info.isDefaultOutput) { @@ -289,21 +330,29 @@ int RtAudioInterface::RtAudioCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double /*streamTime*/, RtAudioStreamStatus /*status*/) { - sample_t* inputBuffer_sample = (sample_t*)inputBuffer; - sample_t* outputBuffer_sample = (sample_t*)outputBuffer; - - // Get input and output buffers - //------------------------------------------------------------------- - for (int i = 0; i < mNumInChans; i++) { - // Input Ports are READ ONLY - mInBuffer[i] = inputBuffer_sample + (nFrames * i); - } - for (int i = 0; i < mNumOutChans; i++) { - // Output Ports are WRITABLE - mOutBuffer[i] = outputBuffer_sample + (nFrames * i); + // TODO: this function may need more changes. As-is I'm not sure this will work + + sample_t* inputBuffer_sample = NULL; + sample_t* outputBuffer_sample = NULL; + + inputBuffer_sample = (sample_t*)inputBuffer; + outputBuffer_sample = (sample_t*)outputBuffer; + + if (inputBuffer_sample != NULL && outputBuffer_sample != NULL) { + // Get input and output buffers + //------------------------------------------------------------------- + for (int i = 0; i < mNumInChans; i++) { + // Input Ports are READ ONLY + mInBuffer[i] = inputBuffer_sample + (nFrames * i); + } + for (int i = 0; i < mNumOutChans; i++) { + // Output Ports are WRITABLE + mOutBuffer[i] = outputBuffer_sample + (nFrames * i); + } + + AudioInterface::callback(mInBuffer, mOutBuffer, nFrames); } - AudioInterface::callback(mInBuffer, mOutBuffer, nFrames); return 0; } @@ -312,8 +361,9 @@ int RtAudioInterface::wrapperRtAudioCallback(void* outputBuffer, void* inputBuff unsigned int nFrames, double streamTime, RtAudioStreamStatus status, void* userData) { - return static_cast(userData)->RtAudioCallback( - outputBuffer, inputBuffer, nFrames, streamTime, status); + RtAudioInterface* interface = static_cast(userData); + return interface->RtAudioCallback(outputBuffer, inputBuffer, nFrames, streamTime, + status); } //******************************************************************************* @@ -321,31 +371,192 @@ void RtAudioInterface::RtAudioErrorCallback(RtAudioError::Type type, const std::string& errorText) { if ((type != RtAudioError::WARNING) && (type != RtAudioError::DEBUG_WARNING)) { - std::cout << '\n' << errorText << '\n' << std::endl; + std::cout << errorText << '\n' << std::endl; throw std::runtime_error(errorText); + } else if (type == RtAudioError::WARNING) { + std::cout << errorText << '\n' << std::endl; + } else if (type == RtAudioError::DEBUG_WARNING) { + std::cout << errorText << '\n' << std::endl; } } //******************************************************************************* -int RtAudioInterface::startProcess() const +int RtAudioInterface::startProcess() { try { - mRtAudio->startStream(); + if (mRtAudio != NULL) { + mRtAudio->startStream(); + } } catch (RtAudioError& e) { - std::cout << '\n' << e.getMessage() << '\n' << std::endl; + std::cout << e.getMessage() << '\n' << std::endl; return (-1); } return (0); } //******************************************************************************* -int RtAudioInterface::stopProcess() const +int RtAudioInterface::stopProcess() { try { - mRtAudio->closeStream(); + if (mRtAudio != NULL) { + mRtAudio->closeStream(); + AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE); + AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE); + } } catch (RtAudioError& e) { - std::cout << '\n' << e.getMessage() << '\n' << std::endl; + std::cout << e.getMessage() << '\n' << std::endl; return (-1); } return 0; } + +//******************************************************************************* +void RtAudioInterface::getDeviceList(QStringList* list, QStringList* categories, + bool isInput) +{ + RtAudio baseRtAudio; + RtAudio::Api baseRtAudioApi = baseRtAudio.getCurrentApi(); + + // Add (default) + list->clear(); + list->append(QStringLiteral("(default)")); + if (categories != NULL) { +#ifdef _WIN32 + switch (baseRtAudioApi) { + case RtAudio::WINDOWS_ASIO: + categories->append(QStringLiteral("Low-Latency (ASIO)")); + break; + case RtAudio::WINDOWS_WASAPI: + categories->append(QStringLiteral("All Devices (Non-ASIO)")); + break; + case RtAudio::WINDOWS_DS: + categories->append(QStringLiteral("All Devices (Non-ASIO)")); + break; + default: + categories->append(QStringLiteral("")); + break; + } +#else + categories->append(QStringLiteral("")); +#endif + } + + // Explicitly add default device + QString defaultDeviceName = ""; + uint32_t defaultDeviceIdx; + if (isInput) { + defaultDeviceIdx = baseRtAudio.getDefaultInputDevice(); + } else { + defaultDeviceIdx = baseRtAudio.getDefaultOutputDevice(); + } + + if (defaultDeviceIdx != 0) { + RtAudio::DeviceInfo info = baseRtAudio.getDeviceInfo(defaultDeviceIdx); + defaultDeviceName = QString::fromStdString(info.name); + } + + if (defaultDeviceName != "") { + list->append(defaultDeviceName); + if (categories != NULL) { +#ifdef _WIN32 + switch (baseRtAudioApi) { + case RtAudio::WINDOWS_ASIO: + categories->append(QStringLiteral("Low-Latency (ASIO)")); + break; + case RtAudio::WINDOWS_WASAPI: + categories->append(QStringLiteral("All Devices (Non-ASIO)")); + break; + case RtAudio::WINDOWS_DS: + categories->append(QStringLiteral("All Devices (Non-ASIO)")); + break; + default: + categories->append(QStringLiteral("")); + break; + } +#else + categories->append(QStringLiteral("")); +#endif + } + } + + std::vector apis; + RtAudio::getCompiledApi(apis); + + for (uint32_t i = 0; i < apis.size(); i++) { + RtAudio::Api api = apis.at(i); + RtAudio rtaudio(api); + unsigned int devices = rtaudio.getDeviceCount(); + for (unsigned int j = 0; j < devices; j++) { + RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(j); + if (info.probed == true) { + // Don't include duplicate entries + if (list->contains(QString::fromStdString(info.name))) { + continue; + } + + // Skip the default device, since we already added it + if (QString::fromStdString(info.name) == defaultDeviceName + && api == baseRtAudioApi) { + continue; + } + + if (isInput && info.inputChannels > 0) { + list->append(QString::fromStdString(info.name)); + } else if (!isInput && info.outputChannels > 0) { + list->append(QString::fromStdString(info.name)); + } + + if (categories == NULL) { + continue; + } + +#ifdef _WIN32 + switch (api) { + case RtAudio::WINDOWS_ASIO: + categories->append("Low-Latency (ASIO)"); + break; + case RtAudio::WINDOWS_WASAPI: + categories->append("All Devices (Non-ASIO)"); + break; + case RtAudio::WINDOWS_DS: + categories->append("All Devices (Non-ASIO)"); + break; + default: + categories->append(""); + break; + } +#else + categories->append(""); +#endif + } + } + } +} + +//******************************************************************************* +void RtAudioInterface::getDeviceInfoFromName(std::string deviceName, int* index, + std::string* api, bool isInput) +{ + std::vector apis; + RtAudio::getCompiledApi(apis); + + for (uint32_t i = 0; i < apis.size(); i++) { + RtAudio rtaudio(apis.at(i)); + unsigned int devices = rtaudio.getDeviceCount(); + for (unsigned int j = 0; j < devices; j++) { + RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(j); + if (info.probed == true && deviceName == info.name) { + if ((isInput && info.inputChannels > 0) + || (!isInput && info.outputChannels > 0)) { + *index = j; + *api = RtAudio::getApiName(rtaudio.getCurrentApi()); + return; + } + } + } + } + + *index = -1; + *api = ""; + return; +} \ No newline at end of file diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h index afcfba0..bb1b746 100644 --- a/src/RtAudioInterface.h +++ b/src/RtAudioInterface.h @@ -40,6 +40,8 @@ #include +#include + #include "AudioInterface.h" #include "jacktrip_globals.h" class JackTrip; // Forward declaration @@ -65,15 +67,17 @@ class RtAudioInterface : public AudioInterface virtual ~RtAudioInterface(); /// \brief List all available audio interfaces, with its properties - virtual void listAllInterfaces(); static void printDevices(); - virtual int getDeviceIdFromName(std::string deviceName, bool isInput); - virtual void setup(); - virtual int startProcess() const; - virtual int stopProcess() const; + virtual void setup(bool verbose = true); + virtual int startProcess(); + virtual int stopProcess(); /// \brief This has no effect in RtAudio virtual void connectDefaultPorts() {} + static void getDeviceList(QStringList* list, QStringList* categories, bool isInput); + static void getDeviceInfoFromName(std::string deviceName, int* index, + std::string* api, bool isInput); + //--------------SETTERS--------------------------------------------- /// \brief This has no effect in RtAudio virtual void setClientName(const QString& /*ClientName*/) {} @@ -90,7 +94,7 @@ class RtAudioInterface : public AudioInterface RtAudioStreamStatus status, void* userData); static void RtAudioErrorCallback(RtAudioError::Type type, const std::string& errorText); - void printDeviceInfo(unsigned int deviceId); + void printDeviceInfo(std::string api, unsigned int deviceId); int mNumInChans; ///< Number of Input Channels int mNumOutChans; ///< Number of Output Channels @@ -98,8 +102,8 @@ class RtAudioInterface : public AudioInterface mInBuffer; ///< Vector of Input buffers/channel read from JACK QVarLengthArray mOutBuffer; ///< Vector of Output buffer/channel to write to JACK - RtAudio* mRtAudio; ///< RtAudio class - unsigned int getDefaultDevice(bool isInput); + RtAudio* mRtAudio; ///< RtAudio class if the input and output device are the same + unsigned int getDefaultDeviceForLinuxPulseAudio(bool isInput); }; #endif // __RTAUDIOINTERFACE_H__ diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index bf1e4c3..bfd862c 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -127,11 +127,7 @@ UdpDataProtocol::~UdpDataProtocol() void UdpDataProtocol::setPeerAddress(const char* peerHostOrIP) { // Get DNS Address -#ifndef _WIN32 - // Don't make the following code conditional on windows - //(Addresses a weird timing bug when in hub client mode) if (!mPeerAddress.setAddress(peerHostOrIP)) { -#endif QHostInfo info = QHostInfo::fromName(peerHostOrIP); if (!info.addresses().isEmpty()) { // use the first IP address @@ -139,9 +135,7 @@ void UdpDataProtocol::setPeerAddress(const char* peerHostOrIP) } // cout << "UdpDataProtocol::setPeerAddress IP Address Number: " // << mPeerAddress.toString().toStdString() << endl; -#ifndef _WIN32 } -#endif // check if the ip address is valid if (mPeerAddress.protocol() == QAbstractSocket::IPv6Protocol) { @@ -324,24 +318,29 @@ int UdpDataProtocol::bindSocket() return sock_fd; } +void UdpDataProtocol::processControlPacket(const char* buf) +{ + // Control signal (currently just check for exit packet); + bool exit = true; + for (int i = 0; i < mControlPacketSize; i++) { + if (buf[i] != char(0xff)) { + exit = false; + i = mControlPacketSize; + } + } + if (exit && !mStopSignalSent) { + mStopSignalSent = true; + emit signalCeaseTransmission(QStringLiteral("Peer Stopped")); + std::cout << "Peer Stopped" << std::endl; + } +} + //******************************************************************************* int UdpDataProtocol::receivePacket(char* buf, const size_t n) { int n_bytes = ::recv(mSocket, buf, n, 0); if (n_bytes == mControlPacketSize) { - // Control signal (currently just check for exit packet); - bool exit = true; - for (int i = 0; i < mControlPacketSize; i++) { - if (buf[i] != char(0xff)) { - exit = false; - i = mControlPacketSize; - } - } - if (exit && !mStopSignalSent) { - mStopSignalSent = true; - emit signalCeaseTransmission(QStringLiteral("Peer Stopped")); - std::cout << "Peer Stopped" << std::endl; - } + processControlPacket(buf); return 0; } return n_bytes; @@ -621,14 +620,39 @@ void UdpDataProtocol::run() mRevivedCount = 0; mStatCount = 0; - //Set up our platform specific polling mechanism. (kqueue, epoll) -#if !defined (MANUAL_POLL) && !defined (_WIN32) -#ifdef __linux__ + //Set up our platform specific polling mechanism. (kqueue, epoll, overlapped I/O) +#if !defined (MANUAL_POLL) +#if defined (__linux__) int epollfd = epoll_create1(0); struct epoll_event change, event; change.events = EPOLLIN; change.data.fd = mSocket; epoll_ctl(epollfd, EPOLL_CTL_ADD, mSocket, &change); +#elif defined (_WIN32) + WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; + WSAOVERLAPPED socketOverlapped; + WSABUF dataBuf; + dataBuf.len = full_redundant_packet_size; + dataBuf.buf = reinterpret_cast(full_redundant_packet); + DWORD recvBytes = 0, flags = 0, index, bytesTransferred = 0; + + eventArray[0] = WSACreateEvent(); + if (eventArray == WSA_INVALID_EVENT) { + emit signalError("Unable to set up network event monitoring"); + cout << "ERROR: Unable to set up network event monitoring" << endl; + mStopped = true; + } + ZeroMemory(&socketOverlapped, sizeof(WSAOVERLAPPED)); + socketOverlapped.hEvent = eventArray[0]; + + if (WSARecv(mSocket, &dataBuf, 1, &recvBytes, &flags, &socketOverlapped, NULL) == SOCKET_ERROR) { + int result = WSAGetLastError(); + if (result != WSA_IO_PENDING) { + emit signalError("Unable to listen for incoming network packets"); + cout << "ERROR: Unable to listen for incoming network packets" << endl; + mStopped = true; + } + } #else int kq = kqueue(); struct kevent change; @@ -649,12 +673,14 @@ void UdpDataProtocol::run() // arrive for a longer time //timeout = UdpSocket.waitForReadyRead(30); // timeout = cc unused! -#if defined (_WIN32) || defined (MANUAL_POLL) +#if defined (MANUAL_POLL) waitForReady(60000); //60 seconds - receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size, - full_packet_size, current_seq_num, last_seq_num, - newer_seq_num); - } + if (receivePacket(reinterpret_cast(full_redundant_packet), full_redundant_packet_size) > 0) { + receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size, + full_packet_size, current_seq_num, last_seq_num, + newer_seq_num); + } + } #else // OLD CODE WITHOUT REDUNDANCY---------------------------------------------------- @@ -671,27 +697,51 @@ void UdpDataProtocol::run() */ //---------------------------------------------------------------------------------- -#ifdef __linux__ +#if defined(_WIN32) + index = WSAWaitForMultipleEvents(1, eventArray, FALSE, 10, FALSE); + if (index == WSA_WAIT_TIMEOUT) { + waitTime += 10; + emit signalWaitingTooLong(waitTime); + } else { + waitTime = 0; + WSAResetEvent(eventArray[index - WSA_WAIT_EVENT_0]); + WSAGetOverlappedResult(mSocket, &socketOverlapped, &bytesTransferred, FALSE, &flags); + if (bytesTransferred == mControlPacketSize) { + processControlPacket(reinterpret_cast(full_redundant_packet)); + } else if (bytesTransferred > 0 ){ + receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size, + full_packet_size, current_seq_num, last_seq_num, + newer_seq_num); + } + WSARecv(mSocket, &dataBuf, 1, &recvBytes, &flags, &socketOverlapped, NULL); + } +#else +#if defined(__linux__) int n = epoll_wait(epollfd, &event, 1, 10); #else int n = kevent(kq, &change, 1, &event, 1, &timeout); #endif if (n > 0) { waitTime = 0; - receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size, - full_packet_size, current_seq_num, last_seq_num, - newer_seq_num); + if (receivePacket(reinterpret_cast(full_redundant_packet), full_redundant_packet_size) > 0) { + receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size, + full_packet_size, current_seq_num, last_seq_num, + newer_seq_num); + } } else { waitTime += 10; emit signalWaitingTooLong(waitTime); } +#endif } -#ifdef __linux__ +#if defined(__linux__) close(epollfd); +#elif defined(_WIN32) + WSACloseEvent(eventArray); #else close(kq); #endif -#endif // _WIN32 || MANUAL_POLL +#endif // MANUAL_POLL break; } case SENDER : { @@ -759,16 +809,9 @@ void UdpDataProtocol::printUdpWaitedTooLong(int wait_msec) //******************************************************************************* void UdpDataProtocol::receivePacketRedundancy( - int8_t* full_redundant_packet, int full_redundant_packet_size, int full_packet_size, + int8_t* full_redundant_packet, [[maybe_unused]] int full_redundant_packet_size, int full_packet_size, uint16_t& current_seq_num, uint16_t& last_seq_num, uint16_t& newer_seq_num) { - // This is blocking until we get a packet... - if (receivePacket(reinterpret_cast(full_redundant_packet), - full_redundant_packet_size) - <= 0) { - return; - } - if (0.0 < mSimulatedLossRate || 0.0 < mSimulatedJitterRate) { double x = mUniformDist(mRndEngine); // Drop packets @@ -845,9 +888,7 @@ void UdpDataProtocol::receivePacketRedundancy( src = dst; } int ok = true; // send audio buf to - ok = (mJackTrip->getBufferStrategy() !=3) ? // ring or jitter - mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size) - : mJackTrip->writeAudioBufferRegulator(src, host_buf_size, last_seq_num, gap_size); + ok = mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size, last_seq_num); if (!ok) { emit signalError("Local and Peer buffer settings are incompatible"); cout << "ERROR: Local and Peer buffer settings are incompatible" << endl; @@ -974,7 +1015,6 @@ void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet, bool UdpDataProtocol::datagramAvailable() { //Currently using a simplified version of the way QUdpSocket checks for datagrams. - //TODO: Consider changing to use poll() or select(). char c; #if defined(_WIN32) //Need to use the winsock version of the function for MSG_PEEK diff --git a/src/UdpDataProtocol.h b/src/UdpDataProtocol.h index a49fda1..77b84f2 100644 --- a/src/UdpDataProtocol.h +++ b/src/UdpDataProtocol.h @@ -96,6 +96,8 @@ class UdpDataProtocol : public DataProtocol void setSocket(int& socket); #endif + void processControlPacket(const char* buf); + /** \brief Receives a packet. It blocks until a packet is received * * This function makes sure we receive a complete packet @@ -104,7 +106,6 @@ class UdpDataProtocol : public DataProtocol * \param n size of packet to receive * \return number of bytes read, -1 on error */ - // virtual int receivePacket(char* buf, const size_t n); virtual int receivePacket(char* buf, const size_t n); /** \brief Sends a packet diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml index d36870c..4dcf4fe 100644 --- a/src/gui/Browse.qml +++ b/src/gui/Browse.qml @@ -18,7 +18,7 @@ Item { property int extraSettingsButtonWidth: 16 property int emptyListMessageWidth: 450 property int createMessageTopMargin: 16 - property int createButtonTopMargin: 48 + property int createButtonTopMargin: 24 property int fontBig: 28 property int fontMedium: 11 @@ -100,7 +100,7 @@ Item { Text { id: emptyListMessage visible: parent.count == 0 && !virtualstudio.showCreateStudio - text: "No studios found that match your filter criteria" + text: "No studios found that match your filter criteria." font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour width: emptyListMessageWidth @@ -110,6 +110,33 @@ Item { 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 @@ -137,7 +164,7 @@ Item { Text { id: createStudioMessage - text: "JackTrip works by connecting your computer's audio to a Virtual Studio. Create your first Studio to get started!" + 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 @@ -267,7 +294,7 @@ Item { border.width: 1 border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke) } - onClicked: window.state = "settings" + onClicked: virtualstudio.windowState = "settings" display: AbstractButton.TextBesideIcon font { family: "Poppins"; diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml index 3cd5209..80e1317 100644 --- a/src/gui/Connected.qml +++ b/src/gui/Connected.qml @@ -70,9 +70,13 @@ Item { } Image { + id: jtlogo x: parent.width - (49 * virtualstudio.uiScale); y: 16 * virtualstudio.uiScale width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale source: "logo.svg" + sourceSize: Qt.size(jtlogo.width,jtlogo.height) + fillMode: Image.PreserveAspectFit + smooth: true } Text { @@ -109,6 +113,9 @@ Item { source: "mic.svg" x: 0; y: 0 width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + sourceSize: Qt.size(mic.width,mic.height) + fillMode: Image.PreserveAspectFit + smooth: true } Colorize { @@ -136,7 +143,7 @@ Item { anchors.top: inputDeviceHeader.bottom anchors.left: inputDeviceHeader.left text: virtualstudio.audioBackend == "JACK" ? - virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice] + virtualstudio.audioBackend : inputComboModel.filter(item => item.type === "element")[virtualstudio.inputDevice].text font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour elide: Text.ElideRight @@ -155,6 +162,9 @@ Item { source: "headphones.svg" x: 0; y: 0 width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + sourceSize: Qt.size(headphones.width,headphones.height) + fillMode: Image.PreserveAspectFit + smooth: true } Colorize { @@ -182,7 +192,7 @@ Item { anchors.top: outputDeviceHeader.bottom anchors.left: outputDeviceHeader.left text: virtualstudio.audioBackend == "JACK" ? - virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice] + virtualstudio.audioBackend : outputComboModel.filter(item => item.type === "element")[virtualstudio.outputDevice].text font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour elide: Text.ElideRight @@ -245,6 +255,9 @@ Item { width: 11.57 * virtualstudio.uiScale; height: 18 * virtualstudio.uiScale anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } source: virtualstudio.inputMuted ? "micoff.svg" : "mic.svg" + sourceSize: Qt.size(micMute.width,micMute.height) + fillMode: Image.PreserveAspectFit + smooth: true } Colorize { anchors.fill: micMute @@ -335,6 +348,9 @@ Item { source: "network.svg" x: 0; y: 0 width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + sourceSize: Qt.size(network.width,network.height) + fillMode: Image.PreserveAspectFit + smooth: true } Colorize { diff --git a/src/gui/Failed.qml b/src/gui/Failed.qml index 8eb4a61..28bafcf 100644 --- a/src/gui/Failed.qml +++ b/src/gui/Failed.qml @@ -71,7 +71,7 @@ Item { border.color: buttonStroke layer.enabled: !backButton.down } - onClicked: { window.state = "browse" } + onClicked: { virtualstudio.windowState = "browse" } width: 256 * virtualstudio.uiScale height: 42 * virtualstudio.uiScale anchors.horizontalCenter: parent.horizontalCenter diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml index 7356df2..2cba8ac 100644 --- a/src/gui/FirstLaunch.qml +++ b/src/gui/FirstLaunch.qml @@ -16,10 +16,14 @@ Item { 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 { @@ -56,7 +60,7 @@ Item { color: shadowColour } } - onClicked: { window.state = "login"; virtualstudio.toVirtualStudio(); } + onClicked: { 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 { @@ -104,7 +108,7 @@ Item { color: shadowColour } } - onClicked: { window.state = "login"; virtualstudio.toStandard(); } + onClicked: { virtualstudio.windowState = "login"; virtualstudio.toStandard(); } x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale Text { diff --git a/src/gui/Login.qml b/src/gui/Login.qml index 0ed3fc0..955cfb1 100644 --- a/src/gui/Login.qml +++ b/src/gui/Login.qml @@ -36,6 +36,9 @@ Item { source: "logo.svg" x: parent.width / 2 - (150 * virtualstudio.uiScale); y: 110 * virtualstudio.uiScale width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale + sourceSize: Qt.size(loginLogo.width,loginLogo.height) + fillMode: Image.PreserveAspectFit + smooth: true } Image { @@ -124,7 +127,7 @@ Item { color: shadowColour } } - onClicked: { window.state = "start" } + onClicked: { virtualstudio.windowState = "start" } anchors.horizontalCenter: parent.horizontalCenter y: 401 * virtualstudio.uiScale width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale diff --git a/src/gui/Meter.qml b/src/gui/Meter.qml index f3a3a3d..64b4450 100644 --- a/src/gui/Meter.qml +++ b/src/gui/Meter.qml @@ -10,7 +10,8 @@ Item { property int clipWidth: 10 * virtualstudio.uiScale required property bool clipped - property string meterColor: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property bool enabled: true + property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : "#EAECEC" property string meterGreen: "#61C554" property string meterYellow: "#F5BF4F" @@ -18,6 +19,10 @@ Item { function getBoxColor (idx, level) { + if (!enabled) { + return meterColor; + } + // Case where the meter should be filled if (level > (idx / bins)) { let fillColor = meterGreen; diff --git a/src/gui/Prompt.svg b/src/gui/Prompt.svg new file mode 100644 index 0000000..110d116 --- /dev/null +++ b/src/gui/Prompt.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml index 7e478af..76c4d0b 100644 --- a/src/gui/Settings.qml +++ b/src/gui/Settings.qml @@ -13,6 +13,7 @@ Item { property int fontBig: 28 property int fontMedium: 13 property int fontSmall: 11 + property int fontExtraSmall: 8 property int leftMargin: 48 property int rightMargin: 16 @@ -27,6 +28,12 @@ Item { property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string warningTextColour: "#DB0A0A" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + + property string errorFlagColour: "#DB0A0A" + + property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" property string settingsGroupView: "Audio" @@ -57,7 +64,7 @@ Item { y: header.height-1 modal: false interactive: false - visible: window.state == "settings" + visible: virtualstudio.windowState == "settings" background: Rectangle { border.color: "#33979797" @@ -78,12 +85,31 @@ Item { id: audioBtn text: "Audio" width: parent.width - contentItem: Label { - text: audioBtn.text - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour + contentItem: Item { + implicitWidth: audioButtonText.implicitWidth + implicitHeight: audioButtonText.implicitHeight + + Label { + id: audioButtonText + text: audioBtn.text + width: Boolean(virtualstudio.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(virtualstudio.devicesError) + } } background: Rectangle { width: parent.width @@ -94,13 +120,22 @@ Item { id: appearanceBtn text: "Appearance" width: parent.width - contentItem: Label { - text: appearanceBtn.text - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour + 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) @@ -110,12 +145,19 @@ Item { id: advancedBtn text: "Advanced" width: parent.width - contentItem: Label { - text: advancedBtn.text - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour + 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 @@ -126,12 +168,20 @@ Item { id: profileBtn text: "Profile" width: parent.width - contentItem: Label { - text: profileBtn.text - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour + 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 @@ -173,7 +223,7 @@ Item { model: backendComboModel currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 onActivated: { virtualstudio.audioBackend = currentText } - x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + x: 234 * virtualstudio.uiScale; y: 48 * virtualstudio.uiScale width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale visible: virtualstudio.selectableBackend } @@ -199,53 +249,67 @@ Item { color: textColour } - ComboBox { - id: inputCombo - model: inputComboModel - currentIndex: virtualstudio.inputDevice - onActivated: { virtualstudio.inputDevice = currentIndex } - x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 148 : 100) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + Text { + anchors.verticalCenter: outputCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Output Device" + font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } visible: virtualstudio.audioBackend != "JACK" - } - - Meter { - id: inputDeviceMeters - anchors.left: backendCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? inputCombo.y + 48 * virtualstudio.uiScale : virtualstudio.uiScale * (virtualstudio.selectableBackend ? 148 : 100) - height: 100 * virtualstudio.uiScale - model: inputMeterModel - clipped: inputClipped + color: textColour } ComboBox { id: outputCombo model: outputComboModel - currentIndex: virtualstudio.outputDevice - onActivated: { virtualstudio.outputDevice = currentIndex } - x: backendCombo.x; y: inputDeviceMeters.y + (48 * virtualstudio.uiScale) + currentIndex: (() => { + let count = 0; + for (let i = 0; i < outputCombo.model.length; i++) { + if (outputCombo.model[i].type === "element") { + count++; + } + + if (count > virtualstudio.outputDevice) { + return i; + } + } + + return 0; + })() + x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 96 : 48) width: backendCombo.width; height: backendCombo.height visible: virtualstudio.audioBackend != "JACK" - } + delegate: ItemDelegate { + required property var modelData + required property int index - Text { - anchors.verticalCenter: inputCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Input Device" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" - color: textColour - } + leftPadding: 0 - Text { - anchors.verticalCenter: outputCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Output Device" - font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" - color: textColour + 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() + virtualstudio.outputDevice = index - outputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: outputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputCombo.model[outputCombo.currentIndex].text + } } Button { @@ -259,7 +323,7 @@ Item { onClicked: { virtualstudio.playOutputAudio() } width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale x: parent.width - (232 * virtualstudio.uiScale) - y: virtualstudio.audioBackend != "JACK" ? outputCombo.y + (60 * virtualstudio.uiScale) : inputDeviceMeters.y + (48 * virtualstudio.uiScale) + y: virtualstudio.audioBackend != "JACK" ? outputCombo.y + (48 * virtualstudio.uiScale) : outputCombo.y + (48 * virtualstudio.uiScale) Text { text: "Test Output Audio" font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } @@ -268,6 +332,81 @@ Item { } } + Text { + anchors.verticalCenter: inputCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale; y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) + text: "Input Device" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.audioBackend != "JACK" + color: textColour + } + + ComboBox { + id: inputCombo + model: inputComboModel + currentIndex: (() => { + let count = 0; + for (let i = 0; i < inputCombo.model.length; i++) { + if (inputCombo.model[i].type === "element") { + count++; + } + + if (count > virtualstudio.inputDevice) { + return i; + } + } + + return 0; + })() + x: backendCombo.x; y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + 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() + virtualstudio.inputDevice = index - inputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputCombo.model[inputCombo.currentIndex].text + } + } + + Meter { + id: inputDeviceMeters + anchors.left: backendCombo.left + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + y: virtualstudio.audioBackend != "JACK" ? inputCombo.y + 48 * virtualstudio.uiScale : virtualstudio.uiScale * (virtualstudio.selectableBackend ? 112 : 64) + height: 100 * virtualstudio.uiScale + model: inputMeterModel + clipped: inputClipped + enabled: !Boolean(virtualstudio.devicesError) + } + Button { id: refreshButton background: Rectangle { @@ -277,7 +416,7 @@ Item { border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) } onClicked: { virtualstudio.refreshDevices() } - x: parent.width - (232 * virtualstudio.uiScale); y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) + x: parent.width - (232 * virtualstudio.uiScale); y: inputDeviceMeters.y + (48 * virtualstudio.uiScale) width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale visible: virtualstudio.audioBackend != "JACK" Text { @@ -288,10 +427,31 @@ Item { } } + Text { + id: devicesWarningOrError + x: leftMargin * virtualstudio.uiScale + y: virtualstudio.audioBackend != "JACK" ? refreshButton.y + (48 * virtualstudio.uiScale) : testOutputAudioButton.y + (48 * virtualstudio.uiScale) + width: parent.width - (64 * virtualstudio.uiScale) + textFormat: Text.RichText + text: (virtualstudio.devicesError || virtualstudio.devicesWarning) + + ((virtualstudio.devicesErrorHelpUrl || virtualstudio.devicesWarningHelpUrl) + ? ` Learn More.` + : "" + ) + onLinkActivated: link => { + virtualstudio.openLink(link) + } + horizontalAlignment: Text.AlignHLeft + wrapMode: Text.WordWrap + color: warningTextColour + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + visible: Boolean(virtualstudio.devicesError || virtualstudio.devicesWarning); + } + Rectangle { id: divider x: leftMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? refreshButton.y + (48 * virtualstudio.uiScale) : testOutputAudioButton.y + (48 * virtualstudio.uiScale) + y: Boolean(virtualstudio.devicesError || virtualstudio.devicesWarning) ? devicesWarningOrError.y + (60 * virtualstudio.uiScale) : refreshButton.y + (60 * virtualstudio.uiScale) width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale color: textColour visible: virtualstudio.audioBackend != "JACK" @@ -388,7 +548,7 @@ Item { border.width: 1 border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke) } - onClicked: { window.state = "login"; virtualstudio.toStandard(); } + onClicked: { virtualstudio.windowState = "login"; virtualstudio.toStandard(); } x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale Text { @@ -512,7 +672,7 @@ Item { border.width: 1 border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke) } - onClicked: { window.state = "login"; virtualstudio.logout() } + onClicked: { virtualstudio.windowState = "login"; virtualstudio.logout() } anchors.horizontalCenter: parent.horizontalCenter y: editButton.y + (48 * virtualstudio.uiScale) width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale @@ -532,7 +692,7 @@ Item { border.width: 1 border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke) } - onClicked: { virtualstudio.testMode = !virtualstudio.testMode; window.state = "login"; virtualstudio.logout() } + onClicked: { virtualstudio.testMode = !virtualstudio.testMode; virtualstudio.windowState = "login"; virtualstudio.logout() } anchors.horizontalCenter: parent.horizontalCenter y: logoutButton.y + (48 * virtualstudio.uiScale) width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale @@ -560,7 +720,7 @@ Item { border.width: 1 border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke) } - onClicked: { window.state = "browse"; virtualstudio.revertSettings() } + onClicked: { virtualstudio.windowState = "browse"; virtualstudio.revertSettings() } anchors.verticalCenter: parent.verticalCenter x: parent.width - (230 * virtualstudio.uiScale) width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale @@ -574,13 +734,14 @@ Item { Button { id: saveButton + enabled: !Boolean(virtualstudio.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: { window.state = "browse"; virtualstudio.applySettings() } + onClicked: { virtualstudio.windowState = "browse"; virtualstudio.applySettings() } anchors.verticalCenter: parent.verticalCenter x: parent.width - (119 * virtualstudio.uiScale) width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale @@ -588,7 +749,7 @@ Item { text: "Save" font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour + color: Boolean(virtualstudio.devicesError) ? disabledButtonTextColour : textColour } } } diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml index b8e8358..65e5f71 100644 --- a/src/gui/Setup.qml +++ b/src/gui/Setup.qml @@ -37,10 +37,12 @@ Item { 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 currShowWarnings: virtualstudio.showWarnings - property string warningScreen: virtualstudio.showWarnings ? "ethernet" : "acknowledged" - + property string warningScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged") + Item { id: ethernetWarningItem width: parent.width; height: parent.height @@ -264,7 +266,13 @@ Item { color: saveButtonShadow } } - onClicked: { virtualstudio.showWarnings = currShowWarnings; warningScreen = "acknowledged" } + onClicked: { + if (permissions.micPermission == "unknown") { + virtualstudio.showWarnings = currShowWarnings; warningScreen = "microphone" + } else { + virtualstudio.showWarnings = currShowWarnings; warningScreen = "acknowledged" + } + } anchors.right: parent.right anchors.rightMargin: 16 * virtualstudio.uiScale anchors.bottomMargin: 16 * virtualstudio.uiScale @@ -319,10 +327,216 @@ Item { } } + Item { + id: requestMicPermissionsItem + width: parent.width; height: parent.height + visible: warningScreen == "microphone" && permissions.micPermission == "unknown" + + Image { + id: microphonePrompt + source: "Prompt.svg" + width: 260 + height: 250 + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + sourceSize: Qt.size(microphonePrompt.width,microphonePrompt.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + 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 + } + + Colorize { + anchors.fill: microphonePrompt + source: microphonePrompt + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + 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 ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: showPromptButton.hovered && !showPromptButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: saveButtonShadow + } + } + 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: (warningScreen == "acknowledged" || warningScreen == "microphone") && permissions.micPermission == "denied" + + Image { + id: noMic + source: "micoff.svg" + width: 109.27 + height: 170 + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + sourceSize: Qt.size(noMic.width,noMic.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Colorize { + anchors.fill: noMic + source: noMic + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + Button { + id: openSettingsButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: openSettingsButton.down ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: openSettingsButton.hovered && !openSettingsButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: saveButtonShadow + } + } + 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 + } + } + Item { id: setupItem width: parent.width; height: parent.height - visible: warningScreen == "acknowledged" + visible: (warningScreen == "acknowledged" || warningScreen == "microphone") && permissions.micPermission == "granted" Text { id: pageTitle @@ -368,11 +582,55 @@ Item { ComboBox { id: outputCombo model: outputComboModel - currentIndex: virtualstudio.outputDevice - onActivated: { virtualstudio.outputDevice = currentIndex } + currentIndex: (() => { + let count = 0; + for (let i = 0; i < outputCombo.model.length; i++) { + if (outputCombo.model[i].type === "element") { + count++; + } + + if (count > virtualstudio.outputDevice) { + return i; + } + } + + return 0; + })() x: backendCombo.x; y: backendCombo.y + virtualstudio.uiScale * (virtualstudio.selectableBackend ? 48 : 0) width: backendCombo.width; height: backendCombo.height visible: virtualstudio.audioBackend != "JACK" + 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() + virtualstudio.outputDevice = index - outputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: outputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputCombo.model[outputCombo.currentIndex].text + } } Text { @@ -431,13 +689,57 @@ Item { ComboBox { id: inputCombo model: inputComboModel - currentIndex: virtualstudio.inputDevice - onActivated: { virtualstudio.inputDevice = currentIndex } + currentIndex: (() => { + let count = 0; + for (let i = 0; i < inputCombo.model.length; i++) { + if (inputCombo.model[i].type === "element") { + count++; + } + + if (count > virtualstudio.inputDevice) { + return i; + } + } + + return 0; + })() anchors.right: parent.right anchors.rightMargin: rightMargin * virtualstudio.uiScale y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) width: parent.width - (234 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale visible: virtualstudio.audioBackend != "JACK" + 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() + virtualstudio.inputDevice = index - inputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputCombo.model[inputCombo.currentIndex].text + } } Text { @@ -459,6 +761,7 @@ Item { height: 100 * virtualstudio.uiScale model: inputMeterModel clipped: inputClipped + enabled: !Boolean(virtualstudio.devicesError) } Slider { @@ -507,18 +810,25 @@ Item { } Text { - anchors.left: outputLabel.left - anchors.right: outputCombo.right - anchors.leftMargin: 16 * virtualstudio.uiScale + anchors.left: inputLabel.left + anchors.right: refreshButton.left anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom + anchors.top: refreshButton.top anchors.bottomMargin: 60 * virtualstudio.uiScale - text: "JackTrip on Windows requires use of an audio device with ASIO drivers. If you do not see your device, you may need to install drivers from your manufacturer." - horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + text: (virtualstudio.devicesError || virtualstudio.devicesWarning) + + ((virtualstudio.devicesErrorHelpUrl || virtualstudio.devicesWarningHelpUrl) + ? ` Learn More.` + : "" + ) + onLinkActivated: link => { + virtualstudio.openLink(link) + } + horizontalAlignment: Text.AlignHLeft wrapMode: Text.WordWrap color: warningText font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - visible: Qt.platform.os == "windows" && virtualstudio.audioBackend != "JACK" + visible: Boolean(virtualstudio.devicesError) || Boolean(virtualstudio.devicesWarning); } Button { @@ -537,7 +847,8 @@ Item { color: saveButtonShadow } } - onClicked: { window.state = "browse"; virtualstudio.applySettings() } + enabled: !Boolean(virtualstudio.devicesError) + onClicked: { virtualstudio.windowState = "browse"; virtualstudio.applySettings() } anchors.right: parent.right anchors.rightMargin: rightMargin * virtualstudio.uiScale anchors.bottomMargin: rightMargin * virtualstudio.uiScale @@ -548,7 +859,7 @@ Item { font.family: "Poppins" font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale font.weight: Font.Bold - color: saveButtonText + color: !Boolean(virtualstudio.devicesError) ? saveButtonText : disabledButtonText anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter } diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml index fffc304..f7f06b9 100644 --- a/src/gui/Studio.qml +++ b/src/gui/Studio.qml @@ -81,13 +81,21 @@ Rectangle { } 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 { @@ -139,8 +147,12 @@ Rectangle { 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 } } @@ -167,16 +179,20 @@ Rectangle { visible: connected || canConnect || canStart onClicked: { if (!connected) { - window.state = "connected"; + virtualstudio.windowState = "connected"; virtualstudio.connectToStudio(index); } else { virtualstudio.disconnect(); } } Image { + id: joinLeave width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } source: connected ? "leave.svg" : "join.svg" + sourceSize: Qt.size(joinLeave.width,joinLeave.height) + fillMode: Image.PreserveAspectFit + smooth: true } } @@ -218,9 +234,13 @@ Rectangle { } visible: true Image { + id: shareImg width: 20 * 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 @@ -284,9 +304,13 @@ Rectangle { } visible: manageable 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 } } diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index 191d544..ad5ebb2 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -49,6 +49,7 @@ #include "../Compressor.h" #include "../CompressorPresets.h" #include "../Limiter.h" +#include "../Meter.h" #include "../Reverb.h" QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) @@ -236,6 +237,11 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) 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); + + m_inputLayout.reset(new QGridLayout(m_ui->inputGroupBox)); + m_outputLayout.reset(new QGridLayout(m_ui->outputGroupBox)); #ifdef RT_AUDIO connect(m_ui->backendComboBox, QOverload::of(&QComboBox::currentIndexChanged), @@ -915,6 +921,9 @@ void QJackTrip::start() // Append any plugins appendPlugins(m_jackTrip.data(), m_ui->channelSendSpinBox->value(), m_ui->channelRecvSpinBox->value()); + // Setup meters (also currently using faust plugins). + createMeters(m_ui->channelSendSpinBox->value(), + (m_ui->channelRecvSpinBox->value())); QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, &QJackTrip::processFinished, Qt::QueuedConnection); @@ -927,6 +936,7 @@ void QJackTrip::start() &QJackTrip::udpWaitingTooLong, Qt::QueuedConnection); QObject::connect(m_jackTrip.data(), &JackTrip::signalQueueLengthChanged, this, &QJackTrip::queueLengthChanged, Qt::QueuedConnection); + m_ui->statusBar->showMessage(QStringLiteral("Waiting for Peer...")); m_ui->disconnectButton->setEnabled(true); #ifdef WAIRTOHUB // WAIR @@ -993,6 +1003,36 @@ void QJackTrip::exit() } } +void QJackTrip::updatedInputMeasurements(const QVector valuesInDb) +{ + for (int i = 0; i < m_inputMeters.count(); i++) { + // Determine decibel reading + qreal dB = m_meterMin; + if (i < valuesInDb.size()) { + dB = std::max(m_meterMin, valuesInDb.at(i)); + } + + // Produce a normalized value from 0 to 1 + float level = (dB - m_meterMin) / (m_meterMax - m_meterMin); + m_inputMeters.at(i)->setLevel(level); + } +} + +void QJackTrip::updatedOutputMeasurements(const QVector valuesInDb) +{ + for (int i = 0; i < m_outputMeters.count(); i++) { + // Determine decibel reading + qreal dB = m_meterMin; + if (i < valuesInDb.size()) { + dB = std::max(m_meterMin, valuesInDb.at(i)); + } + + // Produce a normalized value from 0 to 1 + float level = (dB - m_meterMin) / (m_meterMax - m_meterMin); + m_outputMeters.at(i)->setLevel(level); + } +} + #ifndef NO_VS void QJackTrip::virtualStudioMode() { @@ -1013,7 +1053,20 @@ int QJackTrip::findTab(const QString& tabName) void QJackTrip::enableUi(bool enabled) { - m_ui->optionsTabWidget->setEnabled(enabled); + if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) { + m_ui->optionsTabWidget->setEnabled(enabled); + } else { + if (enabled) { + m_ui->inputGroupBox->setVisible(false); + m_ui->outputGroupBox->setVisible(false); + removeMeters(); + m_ui->optionsTabWidget->setVisible(true); + } else { + m_ui->optionsTabWidget->setVisible(false); + m_ui->inputGroupBox->setVisible(true); + m_ui->outputGroupBox->setVisible(true); + } + } m_ui->typeLabel->setEnabled(enabled); m_ui->typeComboBox->setEnabled(enabled); m_ui->addressLabel->setEnabled( @@ -1394,6 +1447,66 @@ void QJackTrip::appendPlugins(JackTrip* jackTrip, int numSendChannels, } } +void QJackTrip::createMeters(quint32 inputChannels, quint32 outputChannels) +{ + // These pointers are also deleted by AudioInterface. + Meter* inputMeter = new Meter(inputChannels); + Meter* outputMeter = new Meter(outputChannels); + m_jackTrip->appendProcessPluginToNetwork(inputMeter); + m_jackTrip->appendProcessPluginFromNetwork(outputMeter); + + // Create our widgets. + for (quint32 i = 0; i < inputChannels; i++) { + VuMeter* meter = new VuMeter(this); + m_inputMeters.append(meter); + QLabel* label = new QLabel(QString::number(i + 1), this); + m_inputLabels.append(label); + label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + m_inputLayout->addWidget(label, i, 0, 1, 1); + m_inputLayout->addWidget(meter, i, 1, 1, 1); + } + // Effectively add a spacer at the bottom. + m_inputLayout->setRowStretch(inputChannels, 100); + + for (quint32 i = 0; i < outputChannels; i++) { + VuMeter* meter = new VuMeter(this); + m_outputMeters.append(meter); + QLabel* label = new QLabel(QString::number(i + 1), this); + m_outputLabels.append(label); + label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + m_outputLayout->addWidget(label, i, 0, 1, 1); + m_outputLayout->addWidget(meter, i, 1, 1, 1); + } + m_outputLayout->setRowStretch(outputChannels, 100); + + QObject::connect(inputMeter, &Meter::onComputedVolumeMeasurements, this, + &QJackTrip::updatedInputMeasurements); + QObject::connect(outputMeter, &Meter::onComputedVolumeMeasurements, this, + &QJackTrip::updatedOutputMeasurements); +} + +void QJackTrip::removeMeters() +{ + m_inputLayout->setRowStretch(m_inputMeters.count(), 0); + m_outputLayout->setRowStretch(m_outputMeters.count(), 0); + for (int i = 0; i < m_inputLabels.count(); i++) { + delete m_inputLabels.at(i); + } + for (int i = 0; i < m_inputMeters.count(); i++) { + delete m_inputMeters.at(i); + } + for (int i = 0; i < m_outputLabels.count(); i++) { + delete m_outputLabels.at(i); + } + for (int i = 0; i < m_outputMeters.count(); i++) { + delete m_outputMeters.at(i); + } + m_inputLabels.clear(); + m_inputMeters.clear(); + m_outputLabels.clear(); + m_outputMeters.clear(); +} + QString QJackTrip::commandLineFromCurrentOptions() { QString commandLine = QStringLiteral("jacktrip"); @@ -1633,23 +1746,33 @@ QString QJackTrip::commandLineFromCurrentOptions() #ifdef RT_AUDIO void QJackTrip::populateDeviceMenu(QComboBox* menu, bool isInput) { - RtAudio audio; QString previousString = menu->currentText(); menu->clear(); - // std::cout << "previousString: " << previousString.toStdString() << std::endl; menu->addItem(QStringLiteral("(default)")); - unsigned int devices = audio.getDeviceCount(); - RtAudio::DeviceInfo info; - for (unsigned int i = 0; i < devices; i++) { - info = audio.getDeviceInfo(i); - if (info.probed == true) { - if (isInput && info.inputChannels > 0) { - menu->addItem(QString::fromStdString(info.name)); - } else if (!isInput && info.outputChannels > 0) { - menu->addItem(QString::fromStdString(info.name)); + + std::vector apis; + RtAudio::getCompiledApi(apis); + + for (uint32_t i = 0; i < apis.size(); i++) { + RtAudio rtaudio(apis.at(i)); + unsigned int devices = rtaudio.getDeviceCount(); + for (unsigned int j = 0; j < devices; j++) { + RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(j); + if (info.probed == true) { + // Don't include duplicate entries + if (menu->findText(QString::fromStdString(info.name)) != -1) { + continue; + } + + if (isInput && info.inputChannels > 0) { + menu->addItem(QString::fromStdString(info.name)); + } else if (!isInput && info.outputChannels > 0) { + menu->addItem(QString::fromStdString(info.name)); + } } } } + // set the previous value menu->setCurrentText(previousString); } diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h index 4359b31..ff0a75e 100644 --- a/src/gui/qjacktrip.h +++ b/src/gui/qjacktrip.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include "../JackTrip.h" #include "../UdpHubListener.h" #include "messageDialog.h" +#include "vuMeter.h" #ifdef __APPLE__ #include "NoNap.h" @@ -95,6 +97,8 @@ class QJackTrip : public QMainWindow void start(); void stop(); void exit(); + void updatedInputMeasurements(const QVector valuesInDb); + void updatedOutputMeasurements(const QVector valuesInDb); #ifndef NO_VS void virtualStudioMode(); #endif @@ -115,6 +119,8 @@ class QJackTrip : public QMainWindow #endif void appendPlugins(JackTrip* jackTrip, int numSendChannels, int numRecvChannels); + void createMeters(quint32 inputChannels, quint32 outputChannels); + void removeMeters(); QString commandLineFromCurrentOptions(); void showCommandLineMessageBox(); @@ -125,12 +131,22 @@ class QJackTrip : public QMainWindow QScopedPointer m_netManager; QScopedPointer m_statsDialog; QScopedPointer m_debugDialog; + QScopedPointer m_inputLayout; + QScopedPointer m_outputLayout; std::ostream m_realCout; std::ostream m_realCerr; bool m_jackTripRunning; bool m_isExiting; bool m_exitSent; + float m_meterMax = 0.0; + float m_meterMin = -64.0; + + QList m_inputMeters; + QList m_inputLabels; + QList m_outputMeters; + QList m_outputLabels; + QMutex m_requestMutex; QString m_IPv6Address; bool m_hasIPv4Reply; diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index c34366f..9aca57f 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -31,6 +31,7 @@ ethernet.png ohno.png headphones.svg + Prompt.svg network.svg jacktrip.png jacktrip white.png diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui index 9ead504..11a8fb2 100644 --- a/src/gui/qjacktrip.ui +++ b/src/gui/qjacktrip.ui @@ -7,7 +7,7 @@ 0 0 409 - 913 + 961 @@ -19,38 +19,24 @@ - - + + + + + 0 + 0 + + - To connect to a p2p (peer to peer) server you need to run as a p2p client. -To connect to a hub server you need to run as a hub client. + If running as a server, this is the address you should supply to the other clients. +(You will need to enable any port forwarding on your router manually.) - - 2 + + Looking up external IP address... - - - P2P Client - - - - - P2P Server - - - - - Hub Client - - - - - Hub Server - - - + @@ -81,36 +67,10 @@ To connect to a hub server you need to run as a hub client. - - - - If running as a server, this is the address you should supply to the other clients. -(You will need to enable any port forwarding on your router manually.) - - - Looking up external IP address... - - - - - - - - 0 - 0 - - - - Enter the IP address or the hostname of the server you want to connect to. - - - true - - - 5 - - - QComboBox::NoInsert + + + + Input Channels @@ -252,23 +212,7 @@ play from this machine. (Available in client fan out/in and full mix modes.) - - - - - 0 - 0 - - - - &Received from network: - - - channelRecvSpinBox - - - - + @@ -287,26 +231,19 @@ play from this machine. (Available in client fan out/in and full mix modes.) - - - - Number of audio channels to send to the network. - - - 1 - - - 2 - - - - + + + + 0 + 0 + + - &Sent to network: + &Received from network: - channelSendSpinBox + channelRecvSpinBox @@ -322,7 +259,30 @@ play from this machine. (Available in client fan out/in and full mix modes.)&Number of channels - channelRecvSpinBox + channelSendSpinBox + + + + + + + &Sent to network: + + + channelSendSpinBox + + + + + + + Number of audio channels to send to the network. + + + 1 + + + 2 @@ -1805,6 +1765,66 @@ and wetness is the essence of beauty. + + + + To connect to a p2p (peer to peer) server you need to run as a p2p client. +To connect to a hub server you need to run as a hub client. + + + 2 + + + + P2P Client + + + + + P2P Server + + + + + Hub Client + + + + + Hub Server + + + + + + + + + 0 + 0 + + + + Enter the IP address or the hostname of the server you want to connect to. + + + true + + + 5 + + + QComboBox::NoInsert + + + + + + + Output Channels + + + @@ -1826,8 +1846,8 @@ and wetness is the essence of beauty. disconnectButton exitButton optionsTabWidget - channelRecvSpinBox channelSendSpinBox + channelRecvSpinBox autoPatchComboBox patchServerCheckBox upmixCheckBox diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc index 179c85a..5fe08a8 100644 --- a/src/gui/qjacktrip_novs.qrc +++ b/src/gui/qjacktrip_novs.qrc @@ -1,7 +1,7 @@ - about@2x.png - about.png - icon.png + alt/about@2x.png + alt/about.png + alt/icon.png diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp index 6b133ac..d869d97 100644 --- a/src/gui/virtualstudio.cpp +++ b/src/gui/virtualstudio.cpp @@ -113,7 +113,7 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_inMuted = settings.value(QStringLiteral("InMuted"), false).toBool(); m_outMuted = settings.value(QStringLiteral("OutMuted"), false).toBool(); #ifdef RT_AUDIO - m_useRtAudio = settings.value(QStringLiteral("Backend"), 0).toInt() == 1; + m_useRtAudio = settings.value(QStringLiteral("Backend"), 1).toInt() == 1; m_inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); m_bufferSize = settings.value(QStringLiteral("BufferSize"), 128).toInt(); @@ -166,6 +166,22 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) QVariant::fromValue(m_servers)); m_view.engine()->rootContext()->setContextProperty(QStringLiteral("audioInterface"), m_vsAudioInterface.data()); + // Add permissions for Mac +#ifdef __APPLE__ + m_permissions.reset(new VsMacPermissions()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("permissions"), QVariant::fromValue(m_permissions.data())); + if (m_permissions->micPermissionChecked() + && m_permissions->micPermission() == "unknown") { + m_permissions->getMicPermission(); + } + connect(m_permissions.data(), &VsMacPermissions::micPermissionUpdated, this, + &VirtualStudio::startAudio); +#else + m_permissions.reset(new VsPermissions()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("permissions"), QVariant::fromValue(m_permissions.data())); +#endif m_view.engine()->rootContext()->setContextProperty( QStringLiteral("inputMeterModel"), QVariant::fromValue(QVector())); @@ -206,6 +222,17 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) // thread connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio, Qt::QueuedConnection); + connect( + this, &VirtualStudio::studioToJoinChanged, this, + [&]() { + if (!m_studioToJoin.isEmpty()) { + // join studio when studio to join changes + if (readyToJoin()) { + joinStudio(); + } + } + }, + Qt::QueuedConnection); } void VirtualStudio::setStandardWindow(QSharedPointer window) @@ -285,7 +312,14 @@ int VirtualStudio::inputDevice() { #ifdef RT_AUDIO if (m_useRtAudio) { - int index = m_inputDeviceList.indexOf(m_inputDevice); + QStringList filteredInputDeviceList; + for (int i = 0; i < m_inputDeviceList.size(); i++) { + if (m_inputDeviceList.at(i) != "(default)") { + filteredInputDeviceList += m_inputDeviceList.at(i); + } + } + + int index = filteredInputDeviceList.indexOf(m_inputDevice); return index >= 0 ? index : 0; } #endif @@ -298,7 +332,15 @@ void VirtualStudio::setInputDevice([[maybe_unused]] int device) return; } #ifdef RT_AUDIO - m_inputDevice = m_inputDeviceList.at(device); + std::cout << "Setting Input Device: " << device << std::endl; + QStringList filteredInputDeviceList; + for (int i = 0; i < m_inputDeviceList.size(); i++) { + if (m_inputDeviceList.at(i) != "(default)") { + filteredInputDeviceList += m_inputDeviceList.at(i); + } + } + + m_inputDevice = filteredInputDeviceList.at(device); emit inputDeviceSelected(m_inputDevice); #endif } @@ -307,7 +349,14 @@ int VirtualStudio::outputDevice() { #ifdef RT_AUDIO if (m_useRtAudio) { - int index = m_outputDeviceList.indexOf(m_outputDevice); + QStringList filteredOutputDeviceList; + for (int i = 0; i < m_outputDeviceList.size(); i++) { + if (m_outputDeviceList.at(i) != "(default)") { + filteredOutputDeviceList += m_outputDeviceList.at(i); + } + } + + int index = filteredOutputDeviceList.indexOf(m_outputDevice); return index >= 0 ? index : 0; } #endif @@ -320,11 +369,38 @@ void VirtualStudio::setOutputDevice([[maybe_unused]] int device) return; } #ifdef RT_AUDIO - m_outputDevice = m_outputDeviceList.at(device); + QStringList filteredOutputDeviceList; + for (int i = 0; i < m_outputDeviceList.size(); i++) { + if (m_outputDeviceList.at(i) != "(default)") { + filteredOutputDeviceList += m_outputDeviceList.at(i); + } + } + + m_outputDevice = filteredOutputDeviceList.at(device); emit outputDeviceSelected(m_outputDevice); #endif } +QString VirtualStudio::devicesWarning() +{ + return m_devicesWarningMsg; +} + +QString VirtualStudio::devicesError() +{ + return m_devicesErrorMsg; +} + +QString VirtualStudio::devicesWarningHelpUrl() +{ + return m_devicesWarningHelpUrl; +} + +QString VirtualStudio::devicesErrorHelpUrl() +{ + return m_devicesErrorHelpUrl; +} + float VirtualStudio::inputVolume() { return m_inMultiplier; @@ -511,6 +587,17 @@ void VirtualStudio::setShowDeviceSetup(bool show) m_showDeviceSetup = show; } +QString VirtualStudio::windowState() +{ + return m_windowState; +} + +void VirtualStudio::setWindowState(QString state) +{ + m_windowState = state; + emit windowStateUpdated(); +} + bool VirtualStudio::showWarnings() { return m_showWarnings; @@ -528,9 +615,8 @@ void VirtualStudio::setShowWarnings(bool show) if (!m_studioToJoin.isEmpty()) { // device setup view proceeds warning view // if device setup is shown, do not immediately join - if (!m_showDeviceSetup) { + if (readyToJoin()) { // We're done waiting to be on the browse page - m_shouldJoin = true; joinStudio(); } } @@ -596,6 +682,7 @@ QUrl VirtualStudio::studioToJoin() void VirtualStudio::setStudioToJoin(const QUrl& url) { m_studioToJoin = url; + emit studioToJoinChanged(); } bool VirtualStudio::noUpdater() @@ -621,16 +708,6 @@ QString VirtualStudio::failedMessage() return m_failedMessage; } -bool VirtualStudio::shouldJoin() -{ - return m_shouldJoin; -} - -void VirtualStudio::setShouldJoin(bool join) -{ - m_shouldJoin = join; -} - void VirtualStudio::joinStudio() { if (!m_authenticated || m_studioToJoin.isEmpty() || m_servers.isEmpty()) { @@ -643,12 +720,6 @@ void VirtualStudio::joinStudio() return; } - if (!m_shouldJoin) { - // Not time to join yet. - // Waiting until joinStudio is called and m_shouldJoin is true. - return; - } - QString scheme = m_studioToJoin.scheme(); QString path = m_studioToJoin.path(); QString url = m_studioToJoin.toString(); @@ -760,12 +831,18 @@ void VirtualStudio::refreshStudios(int index, bool signalRefresh) void VirtualStudio::refreshDevices() { #ifdef RT_AUDIO - getDeviceList(&m_inputDeviceList, true); - getDeviceList(&m_outputDeviceList, false); - m_view.engine()->rootContext()->setContextProperty( - QStringLiteral("inputComboModel"), QVariant::fromValue(m_inputDeviceList)); - m_view.engine()->rootContext()->setContextProperty( - QStringLiteral("outputComboModel"), QVariant::fromValue(m_outputDeviceList)); + RtAudioInterface::getDeviceList(&m_inputDeviceList, &m_inputDeviceCategories, true); + RtAudioInterface::getDeviceList(&m_outputDeviceList, &m_outputDeviceCategories, + false); + + QVariant inputComboModel = + formatDeviceList(m_inputDeviceList, m_inputDeviceCategories); + QVariant outputComboModel = + formatDeviceList(m_outputDeviceList, m_outputDeviceCategories); + m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputComboModel"), + inputComboModel); + m_view.engine()->rootContext()->setContextProperty(QStringLiteral("outputComboModel"), + outputComboModel); // Make sure we keep our current settings if the device still exists if (!m_inputDeviceList.contains(m_inputDevice)) { @@ -833,7 +910,6 @@ void VirtualStudio::applySettings() // which can display upon opening the app from join link if (!m_studioToJoin.isEmpty()) { // We're done waiting to be on the browse page - m_shouldJoin = true; joinStudio(); } } @@ -1079,6 +1155,9 @@ void VirtualStudio::disconnect() m_vsAudioInterface->startProcess(); } + + m_connectionState = QStringLiteral("Disconnected"); + emit connectionStateChanged(); } void VirtualStudio::manageStudio(int studioIndex) @@ -1111,6 +1190,12 @@ void VirtualStudio::showAbout() about.exec(); } +void VirtualStudio::openLink(const QString& link) +{ + QUrl url = QUrl(link); + QDesktopServices::openUrl(url); +} + void VirtualStudio::exit() { m_refreshTimer.stop(); @@ -1149,42 +1234,13 @@ void VirtualStudio::slotAuthSucceded() m_device = new VsDevice(m_authenticator.data(), m_testMode); m_device->registerApp(); - if (m_vsAudioInterface.isNull()) { - m_vsAudioInterface.reset(new VsAudioInterface()); - m_view.engine()->rootContext()->setContextProperty( - QStringLiteral("audioInterface"), m_vsAudioInterface.data()); +#ifdef __APPLE__ + if (m_permissions->micPermission() == "granted") { + startAudio(); } -#ifdef RT_AUDIO - m_vsAudioInterface->setInputDevice(m_inputDevice); - m_vsAudioInterface->setOutputDevice(m_outputDevice); - m_vsAudioInterface->setAudioInterfaceMode(m_useRtAudio); +#else + startAudio(); #endif - m_vsAudioInterface->setupAudio(); - - connect(this, &VirtualStudio::inputDeviceChanged, m_vsAudioInterface.data(), - &VsAudioInterface::setInputDevice); - connect(this, &VirtualStudio::inputDeviceSelected, m_vsAudioInterface.data(), - &VsAudioInterface::setInputDevice); - connect(this, &VirtualStudio::outputDeviceChanged, m_vsAudioInterface.data(), - &VsAudioInterface::setOutputDevice); - connect(this, &VirtualStudio::outputDeviceSelected, m_vsAudioInterface.data(), - &VsAudioInterface::setOutputDevice); - connect(this, &VirtualStudio::audioBackendChanged, m_vsAudioInterface.data(), - &VsAudioInterface::setAudioInterfaceMode); - connect(this, &VirtualStudio::triggerPlayOutputAudio, m_vsAudioInterface.data(), - &VsAudioInterface::triggerPlayback); - connect(m_vsAudioInterface.data(), &VsAudioInterface::newVolumeMeterMeasurements, - this, &VirtualStudio::updatedInputVuMeasurements); - connect(m_vsAudioInterface.data(), &VsAudioInterface::errorToProcess, this, - &VirtualStudio::processError); - - m_vsAudioInterface->setupPlugins(); - - m_view.engine()->rootContext()->setContextProperty( - QStringLiteral("inputMeterModel"), - QVariant::fromValue(QVector(m_vsAudioInterface->getNumInputChannels()))); - - m_vsAudioInterface->startProcess(); if (m_userId.isEmpty()) { getUserId(); @@ -1203,8 +1259,8 @@ void VirtualStudio::slotAuthSucceded() if (!m_studioToJoin.isEmpty()) { // FTUX shows warnings and device setup views // if any of these enabled, do not immediately join - if (!m_showDeviceSetup) { - // Don't need to set m_shouldJoin because it's default true + if (readyToJoin()) { + // We should join in this case joinStudio(); } } @@ -1361,6 +1417,34 @@ void VirtualStudio::updatedStats(const QJsonObject& stats) return; } +void VirtualStudio::updatedDevicesErrorMsg(const QString& msg) +{ + m_devicesErrorMsg = msg; + emit devicesErrorChanged(); + return; +} + +void VirtualStudio::updatedDevicesWarningMsg(const QString& msg) +{ + m_devicesWarningMsg = msg; + emit devicesWarningChanged(); + return; +} + +void VirtualStudio::updatedDevicesErrorHelpUrl(const QString& url) +{ + m_devicesErrorHelpUrl = url; + emit devicesErrorHelpUrlChanged(); + return; +} + +void VirtualStudio::updatedDevicesWarningHelpUrl(const QString& url) +{ + m_devicesWarningHelpUrl = url; + emit devicesWarningHelpUrlChanged(); + return; +} + void VirtualStudio::updatedInputVuMeasurements(const QVector& valuesInDecibels) { QJsonArray uiValues; @@ -1764,27 +1848,58 @@ void VirtualStudio::getUserMetadata() }); } -#ifdef RT_AUDIO -void VirtualStudio::getDeviceList(QStringList* list, bool isInput) -{ - RtAudio audio; - list->clear(); - list->append(QStringLiteral("(default)")); - - unsigned int devices = audio.getDeviceCount(); - RtAudio::DeviceInfo info; - for (unsigned int i = 0; i < devices; i++) { - info = audio.getDeviceInfo(i); - if (info.probed == true) { - if (isInput && info.inputChannels > 0) { - list->append(QString::fromStdString(info.name)); - } else if (!isInput && info.outputChannels > 0) { - list->append(QString::fromStdString(info.name)); - } - } +void VirtualStudio::startAudio() +{ +#ifdef __APPLE__ + if (m_permissions->micPermission() != "granted") { + return; } -} #endif + if (m_vsAudioInterface.isNull()) { + m_vsAudioInterface.reset(new VsAudioInterface()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("audioInterface"), m_vsAudioInterface.data()); + } +#ifdef RT_AUDIO + m_vsAudioInterface->setInputDevice(m_inputDevice); + m_vsAudioInterface->setOutputDevice(m_outputDevice); + m_vsAudioInterface->setAudioInterfaceMode(m_useRtAudio); +#endif + connect(m_vsAudioInterface.data(), &VsAudioInterface::devicesErrorMsgChanged, this, + &VirtualStudio::updatedDevicesErrorMsg); + connect(m_vsAudioInterface.data(), &VsAudioInterface::devicesWarningMsgChanged, this, + &VirtualStudio::updatedDevicesWarningMsg); + connect(m_vsAudioInterface.data(), &VsAudioInterface::devicesErrorHelpUrlChanged, + this, &VirtualStudio::updatedDevicesErrorHelpUrl); + connect(m_vsAudioInterface.data(), &VsAudioInterface::devicesWarningHelpUrlChanged, + this, &VirtualStudio::updatedDevicesWarningHelpUrl); + m_vsAudioInterface->setupAudio(); + + connect(this, &VirtualStudio::inputDeviceChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setInputDevice); + connect(this, &VirtualStudio::inputDeviceSelected, m_vsAudioInterface.data(), + &VsAudioInterface::setInputDevice); + connect(this, &VirtualStudio::outputDeviceChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setOutputDevice); + connect(this, &VirtualStudio::outputDeviceSelected, m_vsAudioInterface.data(), + &VsAudioInterface::setOutputDevice); + connect(this, &VirtualStudio::audioBackendChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setAudioInterfaceMode); + connect(this, &VirtualStudio::triggerPlayOutputAudio, m_vsAudioInterface.data(), + &VsAudioInterface::triggerPlayback); + connect(m_vsAudioInterface.data(), &VsAudioInterface::newVolumeMeterMeasurements, + this, &VirtualStudio::updatedInputVuMeasurements); + connect(m_vsAudioInterface.data(), &VsAudioInterface::errorToProcess, this, + &VirtualStudio::processError); + + m_vsAudioInterface->setupPlugins(); + + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputMeterModel"), + QVariant::fromValue(QVector(m_vsAudioInterface->getNumInputChannels()))); + + m_vsAudioInterface->startProcess(); +} void VirtualStudio::stopStudio() { @@ -1807,6 +1922,64 @@ void VirtualStudio::stopStudio() }); } +bool VirtualStudio::readyToJoin() +{ + return m_windowState == "browse" + && (m_connectionState == QStringLiteral("Waiting") + || m_connectionState == QStringLiteral("Disconnected")); +} + +#ifdef RT_AUDIO +QVariant VirtualStudio::formatDeviceList(const QStringList& devices, + const QStringList& categories) +{ + QStringList filteredDevices; + QStringList filteredCategories; + + for (int i = 0; i < devices.size(); i++) { + if (!devices[i].contains("(default)")) { + filteredDevices += devices[i]; + filteredCategories += categories[i]; + } + } + + QStringList uniqueCategories = QStringList(filteredCategories); + uniqueCategories.removeDuplicates(); + + bool containsCategories = true; + if (uniqueCategories.size() == 0) { + containsCategories = false; + } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") { + containsCategories = false; + } + + QVariantList items = QVariantList(); + for (int i = 0; i < uniqueCategories.size(); i++) { + QString category = uniqueCategories.at(i); + + if (containsCategories) { + QJsonObject header = QJsonObject(); + header.insert(QString::fromStdString("text"), uniqueCategories.at(i)); + header.insert(QString::fromStdString("type"), + QString::fromStdString("header")); + items.push_back(QVariant(QJsonValue(header))); + } + + for (int j = 0; j < filteredDevices.size(); j++) { + if (filteredCategories.at(j).toStdString() == category.toStdString()) { + QJsonObject element = QJsonObject(); + element.insert(QString::fromStdString("text"), filteredDevices.at(j)); + element.insert(QString::fromStdString("type"), + QString::fromStdString("element")); + items.push_back(QVariant(QJsonValue(element))); + } + } + } + + return QVariant(items); +} +#endif + VirtualStudio::~VirtualStudio() { for (int i = 0; i < m_servers.count(); i++) { diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h index 693b911..0866c2d 100644 --- a/src/gui/virtualstudio.h +++ b/src/gui/virtualstudio.h @@ -60,6 +60,9 @@ #ifdef __APPLE__ #include "NoNap.h" +#include "vsMacPermissions.h" +#else +#include "vsPermissions.h" #endif class QJackTrip; @@ -78,6 +81,14 @@ class VirtualStudio : public QObject int inputDevice READ inputDevice WRITE setInputDevice NOTIFY inputDeviceChanged) Q_PROPERTY(int outputDevice READ outputDevice WRITE setOutputDevice NOTIFY outputDeviceChanged) + + Q_PROPERTY(QString devicesWarning READ devicesWarning NOTIFY devicesWarningChanged) + Q_PROPERTY(QString devicesError READ devicesError NOTIFY devicesErrorChanged) + Q_PROPERTY(QString devicesWarningHelpUrl READ devicesWarningHelpUrl NOTIFY + devicesWarningHelpUrlChanged) + Q_PROPERTY(QString devicesErrorHelpUrl READ devicesErrorHelpUrl NOTIFY + devicesErrorHelpUrlChanged) + Q_PROPERTY( int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) Q_PROPERTY(int bufferStrategy READ bufferStrategy WRITE setBufferStrategy NOTIFY @@ -107,14 +118,14 @@ class VirtualStudio : public QObject Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT) Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT) Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged) - Q_PROPERTY( - bool shouldJoin READ shouldJoin WRITE setShouldJoin NOTIFY shouldJoinChanged) Q_PROPERTY( float inputVolume READ inputVolume WRITE setInputVolume NOTIFY updatedInputVolume) Q_PROPERTY(float outputVolume READ outputVolume WRITE setOutputVolume NOTIFY updatedOutputVolume) Q_PROPERTY( bool inputMuted READ inputMuted WRITE setInputMuted NOTIFY updatedInputMuted) + Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY + windowStateUpdated) public: explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr); @@ -135,6 +146,10 @@ class VirtualStudio : public QObject void setInputDevice(int device); int outputDevice(); void setOutputDevice(int device); + QString devicesWarning(); + QString devicesError(); + QString devicesWarningHelpUrl(); + QString devicesErrorHelpUrl(); int bufferSize(); void setBufferSize(int index); int bufferStrategy(); @@ -170,12 +185,11 @@ class VirtualStudio : public QObject bool noUpdater(); bool psiBuild(); QString failedMessage(); - bool shouldJoin(); - void setShouldJoin(bool join); float inputVolume(); float outputVolume(); bool inputMuted(); bool outputMuted(); + QString windowState(); public slots: void toStandard(); @@ -194,12 +208,14 @@ class VirtualStudio : public QObject void createStudio(); void editProfile(); void showAbout(); + void openLink(const QString& url); void updatedInputVuMeasurements(const QVector& valuesInDecibels); void updatedOutputVuMeasurements(const QVector& valuesInDecibels); void setInputVolume(float multiplier); void setOutputVolume(float multiplier); void setInputMuted(bool muted); void setOutputMuted(bool muted); + void setWindowState(QString state); void exit(); signals: @@ -217,6 +233,10 @@ class VirtualStudio : public QObject void outputDeviceChanged(QString device); void inputDeviceSelected(QString device); void outputDeviceSelected(QString device); + void devicesWarningChanged(); + void devicesErrorChanged(); + void devicesWarningHelpUrlChanged(); + void devicesErrorHelpUrlChanged(); void triggerPlayOutputAudio(); void bufferSizeChanged(); void bufferStrategyChanged(); @@ -238,11 +258,12 @@ class VirtualStudio : public QObject void signalExit(); void periodicRefresh(); void failedMessageChanged(); - void shouldJoinChanged(); + void studioToJoinChanged(); void updatedInputVolume(float multiplier); void updatedOutputVolume(float multiplier); void updatedInputMuted(bool muted); void updatedOutputMuted(bool muted); + void windowStateUpdated(); private slots: void slotAuthSucceded(); @@ -255,6 +276,11 @@ class VirtualStudio : public QObject void launchBrowser(const QUrl& url); void joinStudio(); void updatedStats(const QJsonObject& stats); + void startAudio(); + void updatedDevicesErrorMsg(const QString& msg); + void updatedDevicesWarningMsg(const QString& msg); + void updatedDevicesErrorHelpUrl(const QString& url); + void updatedDevicesWarningHelpUrl(const QString& url); private: void setupAuthenticator(); @@ -266,14 +292,14 @@ class VirtualStudio : public QObject void getSubscriptions(); void getRegions(); void getUserMetadata(); + void stopStudio(); + bool readyToJoin(); #ifdef RT_AUDIO - void getDeviceList(QStringList* list, bool isInput); + QVariant formatDeviceList(const QStringList& devices, const QStringList& categories); #endif - void stopStudio(); bool m_showFirstRun = false; bool m_checkSsl = true; - bool m_shouldJoin = true; QString m_updateChannel; QString m_refreshToken; QString m_userId; @@ -334,6 +360,12 @@ class VirtualStudio : public QObject QTimer m_inputClipTimer; QTimer m_outputClipTimer; + QString m_devicesWarningMsg = QStringLiteral(""); + QString m_devicesErrorMsg = QStringLiteral(""); + QString m_devicesWarningHelpUrl = QStringLiteral(""); + QString m_devicesErrorHelpUrl = QStringLiteral(""); + QString m_windowState = QStringLiteral("login"); + float m_meterMax = 0.0; float m_meterMin = -64.0; @@ -347,6 +379,8 @@ class VirtualStudio : public QObject #ifdef RT_AUDIO QStringList m_inputDeviceList; QStringList m_outputDeviceList; + QStringList m_inputDeviceCategories; + QStringList m_outputDeviceCategories; QString m_inputDevice; QString m_outputDevice; quint16 m_bufferSize; @@ -371,6 +405,8 @@ class VirtualStudio : public QObject #ifdef __APPLE__ NoNap m_noNap; #endif + + QSharedPointer m_permissions; }; #endif // VIRTUALSTUDIO_H diff --git a/src/gui/vs.qml b/src/gui/vs.qml index fec620b..b694d56 100644 --- a/src/gui/vs.qml +++ b/src/gui/vs.qml @@ -8,7 +8,7 @@ Rectangle { width: 696 height: 577 color: backgroundColour - state: virtualstudio.showFirstRun ? "start" : "login" + state: virtualstudio.showFirstRun ? "start" : virtualstudio.windowState anchors.fill: parent id: window @@ -127,24 +127,22 @@ Rectangle { target: virtualstudio onAuthSucceeded: { if (virtualstudio.showDeviceSetup) { - virtualstudio.shouldJoin = false; - window.state = "setup"; + virtualstudio.windowState = "setup"; } else { - virtualstudio.shouldJoin = true; - window.state = "browse"; + virtualstudio.windowState = "browse"; } } onAuthFailed: { loginScreen.failTextVisible = true; } onConnected: { - window.state = "connected"; + virtualstudio.windowState = "connected"; } onFailed: { - window.state = "failed"; + virtualstudio.windowState = "failed"; } onDisconnected: { - window.state = "browse"; + virtualstudio.windowState = "browse"; } } } diff --git a/src/gui/vsAudioInterface.cpp b/src/gui/vsAudioInterface.cpp index b21df2d..df0c7a7 100644 --- a/src/gui/vsAudioInterface.cpp +++ b/src/gui/vsAudioInterface.cpp @@ -111,7 +111,23 @@ void VsAudioInterface::setupAudio() if (gVerboseFlag) std::cout << " JackTrip:setupAudio before m_audioInterface->setup" << std::endl; - m_audioInterface->setup(); + m_audioInterface->setup(true); + + std::string devicesWarningMsg = m_audioInterface->getDevicesWarningMsg(); + std::string devicesErrorMsg = m_audioInterface->getDevicesErrorMsg(); + + if (devicesWarningMsg != "") { + qDebug() << "Devices Warning: " + << QString::fromStdString(devicesWarningMsg); + } + + if (devicesErrorMsg != "") { + qDebug() << "Devices Error: " << QString::fromStdString(devicesErrorMsg); + } + + updateDevicesWarningMsg(QString::fromStdString(devicesWarningMsg)); + updateDevicesErrorMsg(QString::fromStdString(devicesErrorMsg)); + if (gVerboseFlag) std::cout << " JackTrip:setupAudio before m_audioInterface->getSampleRate" @@ -138,12 +154,29 @@ void VsAudioInterface::setupAudio() m_audioInterface->setInputDevice(m_inputDeviceName); m_audioInterface->setOutputDevice(m_outputDeviceName); m_audioInterface->setBufferSizeInSamples(m_audioBufferSize); - m_audioInterface->setup(); + + m_audioInterface->setup(true); // Setup might have reduced number of channels m_numAudioChansIn = m_audioInterface->getNumInputChannels(); m_numAudioChansOut = m_audioInterface->getNumOutputChannels(); // Setup might have changed buffer size m_audioBufferSize = m_audioInterface->getBufferSizeInSamples(); + + std::string devicesWarningMsg = m_audioInterface->getDevicesWarningMsg(); + std::string devicesErrorMsg = m_audioInterface->getDevicesErrorMsg(); + + if (devicesWarningMsg != "") { + qDebug() << "Devices Warning: " + << QString::fromStdString(devicesWarningMsg); + } + + if (devicesErrorMsg != "") { + qDebug() << "Devices Error: " << QString::fromStdString(devicesErrorMsg); + } + + updateDevicesWarningMsg(QString::fromStdString(devicesWarningMsg)); + updateDevicesErrorMsg(QString::fromStdString(devicesErrorMsg)); + #endif #endif } else if (m_audioInterfaceMode == VsAudioInterface::RTAUDIO) { @@ -155,12 +188,41 @@ void VsAudioInterface::setupAudio() m_audioInterface->setInputDevice(m_inputDeviceName); m_audioInterface->setOutputDevice(m_outputDeviceName); m_audioInterface->setBufferSizeInSamples(m_audioBufferSize); - m_audioInterface->setup(); + + m_audioInterface->setup(true); // Setup might have reduced number of channels m_numAudioChansIn = m_audioInterface->getNumInputChannels(); m_numAudioChansOut = m_audioInterface->getNumOutputChannels(); // Setup might have changed buffer size m_audioBufferSize = m_audioInterface->getBufferSizeInSamples(); + + std::string devicesWarningMsg = m_audioInterface->getDevicesWarningMsg(); + std::string devicesErrorMsg = m_audioInterface->getDevicesErrorMsg(); + std::string devicesWarningHelpUrl = + m_audioInterface->getDevicesWarningHelpUrl(); + std::string devicesErrorHelpUrl = m_audioInterface->getDevicesErrorHelpUrl(); + + if (devicesWarningMsg != "") { + qDebug() << "Devices Warning: " + << QString::fromStdString(devicesWarningMsg); + if (devicesWarningHelpUrl != "") { + qDebug() << "Learn More: " + << QString::fromStdString(devicesWarningHelpUrl); + } + } + + if (devicesErrorMsg != "") { + qDebug() << "Devices Error: " << QString::fromStdString(devicesErrorMsg); + if (devicesErrorHelpUrl != "") { + qDebug() << "Learn More: " + << QString::fromStdString(devicesErrorHelpUrl); + } + } + + updateDevicesWarningMsg(QString::fromStdString(devicesWarningMsg)); + updateDevicesErrorMsg(QString::fromStdString(devicesErrorMsg)); + updateDevicesWarningHelpUrl(QString::fromStdString(devicesWarningHelpUrl)); + updateDevicesErrorHelpUrl(QString::fromStdString(devicesErrorHelpUrl)); #endif } @@ -309,7 +371,7 @@ void VsAudioInterface::startProcess() { if (!m_audioInterface.isNull() && !m_audioActive) { try { - m_audioInterface->initPlugins(); + m_audioInterface->initPlugins(false); m_audioInterface->startProcess(); if (m_audioInterfaceMode == VsAudioInterface::JACK) { m_audioInterface->connectDefaultPorts(); @@ -381,3 +443,27 @@ void VsAudioInterface::setOutputMuted(bool muted) settings.endGroup(); emit updatedOutputMuted(muted); } + +void VsAudioInterface::updateDevicesErrorMsg(const QString& msg) +{ + emit devicesErrorMsgChanged(msg); + return; +} + +void VsAudioInterface::updateDevicesWarningMsg(const QString& msg) +{ + emit devicesWarningMsgChanged(msg); + return; +} + +void VsAudioInterface::updateDevicesWarningHelpUrl(const QString& url) +{ + emit devicesWarningHelpUrlChanged(url); + return; +} + +void VsAudioInterface::updateDevicesErrorHelpUrl(const QString& url) +{ + emit devicesErrorHelpUrlChanged(url); + return; +} diff --git a/src/gui/vsAudioInterface.h b/src/gui/vsAudioInterface.h index 507bd16..f4660d1 100644 --- a/src/gui/vsAudioInterface.h +++ b/src/gui/vsAudioInterface.h @@ -109,6 +109,10 @@ class VsAudioInterface : public QObject void modeUpdated(); void newVolumeMeterMeasurements(QVector values); void errorToProcess(const QString& errorMessage); + void devicesErrorMsgChanged(const QString& msg); + void devicesWarningMsgChanged(const QString& msg); + void devicesErrorHelpUrlChanged(const QString& url); + void devicesWarningHelpUrlChanged(const QString& url); private slots: // void refreshAudioStream(); @@ -138,6 +142,11 @@ class VsAudioInterface : public QObject Volume* m_inputVolumePlugin; Volume* m_outputVolumePlugin; Tone* m_outputTonePlugin; + + void updateDevicesErrorMsg(const QString& msg); + void updateDevicesWarningMsg(const QString& msg); + void updateDevicesErrorHelpUrl(const QString& url); + void updateDevicesWarningHelpUrl(const QString& url); }; #endif // VSDAUDIOINTERFACE_H diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp index 9b39725..3bc63b5 100644 --- a/src/gui/vsDevice.cpp +++ b/src/gui/vsDevice.cpp @@ -69,7 +69,7 @@ VsDevice::VsDevice(QOAuth2AuthorizationCodeFlow* authenticator, bool testMode, // Set server levels to stored versions QJsonObject json = { - {QLatin1String("captureVolume"), m_captureVolume * 100.0}, + {QLatin1String("captureVolume"), (int)(m_captureVolume * 100.0)}, {QLatin1String("captureMute"), m_captureMute}, }; QJsonDocument request = QJsonDocument(json); @@ -320,7 +320,7 @@ void VsDevice::sendLevels() { // Add latest volume and mute values to heartbeat body QJsonObject json = { - {QLatin1String("captureVolume"), (int)(m_captureVolume * 100)}, + {QLatin1String("captureVolume"), (int)(m_captureVolume * 100.0)}, {QLatin1String("captureMute"), m_captureMute}, }; QJsonDocument request = QJsonDocument(json); diff --git a/src/gui/vsMacPermissions.h b/src/gui/vsMacPermissions.h new file mode 100644 index 0000000..1360fa5 --- /dev/null +++ b/src/gui/vsMacPermissions.h @@ -0,0 +1,64 @@ +//***************************************************************** +/* + 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 + +#include +#include +#include + +#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__ diff --git a/src/gui/vsMacPermissions.mm b/src/gui/vsMacPermissions.mm new file mode 100644 index 0000000..a29c4ba --- /dev/null +++ b/src/gui/vsMacPermissions.mm @@ -0,0 +1,104 @@ +//***************************************************************** +/* + 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 +#include +#include +#include +#include + +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")); +} diff --git a/src/gui/vsPermissions.cpp b/src/gui/vsPermissions.cpp new file mode 100644 index 0000000..a978a5a --- /dev/null +++ b/src/gui/vsPermissions.cpp @@ -0,0 +1,68 @@ +//***************************************************************** +/* + 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 +#include +#include + +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(); +} diff --git a/src/gui/vsPermissions.h b/src/gui/vsPermissions.h new file mode 100644 index 0000000..cec2d97 --- /dev/null +++ b/src/gui/vsPermissions.h @@ -0,0 +1,70 @@ +//***************************************************************** +/* + 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 +#include +#include + +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__ diff --git a/src/gui/vuMeter.cpp b/src/gui/vuMeter.cpp new file mode 100644 index 0000000..36736c7 --- /dev/null +++ b/src/gui/vuMeter.cpp @@ -0,0 +1,75 @@ +//***************************************************************** +/* + QJackTrip: Bringing a graphical user interface to JackTrip, a + system for high quality audio network performance over the + internet. + + Copyright (c) 2022 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 . +*/ +//***************************************************************** + +#include "vuMeter.h" + +#include + +VuMeter::VuMeter(QWidget* parent) : QWidget(parent), m_level(0) +{ + m_greenOn.setRgb(97, 197, 84); + m_greenOff.setRgb(29, 67, 24); + m_yellowOn.setRgb(245, 191, 79); + m_yellowOff.setRgb(85, 65, 22); + m_redOn.setRgb(242, 27, 27); + m_redOff.setRgb(84, 4, 4); +} + +void VuMeter::setLevel(qreal level) +{ + m_level = level; + update(); +} + +void VuMeter::paintEvent([[maybe_unused]] QPaintEvent* event) +{ + quint32 binWidth = std::floor((width() - ((m_bins - 1) * m_margins)) / m_bins); + QPainter painter(this); + + painter.setPen(Qt::NoPen); + quint32 level = std::round(m_level * (m_bins + 1)); + for (quint32 i = 0; i < m_bins; i++) { + bool on = level > i; + if (on) { + if (i < 9) { + painter.setBrush(m_greenOn); + } else if (i < 12) { + painter.setBrush(m_yellowOn); + } else { + painter.setBrush(m_redOn); + } + } else { + if (i < 9) { + painter.setBrush(m_greenOff); + } else if (i < 12) { + painter.setBrush(m_yellowOff); + } else { + painter.setBrush(m_redOff); + } + } + + painter.drawRect((binWidth + m_margins) * i, 0, binWidth, height()); + } +} diff --git a/src/gui/vuMeter.h b/src/gui/vuMeter.h new file mode 100644 index 0000000..735dc14 --- /dev/null +++ b/src/gui/vuMeter.h @@ -0,0 +1,58 @@ +//***************************************************************** +/* + QJackTrip: Bringing a graphical user interface to JackTrip, a + system for high quality audio network performance over the + internet. + + Copyright (c) 2022 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 . +*/ +//***************************************************************** + +#ifndef VUMETER_H +#define VUMETER_H + +#include +#include + +class VuMeter : public QWidget +{ + Q_OBJECT + + public: + VuMeter(QWidget* parent = nullptr); + ~VuMeter() override = default; + + void setLevel(qreal level); + + protected: + void paintEvent(QPaintEvent* event) override; + + private: + qreal m_level; + QColor m_greenOn; + QColor m_greenOff; + QColor m_yellowOn; + QColor m_yellowOff; + QColor m_redOn; + QColor m_redOff; + + quint32 m_bins = 15; + quint32 m_margins = 2; +}; + +#endif // VUMETER_H diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index 64b553a..6ed3013 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "1.6.6"; ///< JackTrip version +constexpr const char* const gVersion = "1.6.7"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/win/jacktrip.wxs b/win/jacktrip.wxs index 7617489..565f1ac 100644 --- a/win/jacktrip.wxs +++ b/win/jacktrip.wxs @@ -48,6 +48,7 @@ Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + -- 2.30.2