From: IOhannes m zmölnig Date: Sun, 17 Jul 2022 19:44:45 +0000 (+0200) Subject: New upstream version 1.6.1+ds0 X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1~1^2~9^2~20 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=8f6b7f99e3afd1c8cfc50c4c822d4f517b59fbb6;p=jacktrip.git New upstream version 1.6.1+ds0 --- diff --git a/.clang-format-ignore b/.clang-format-ignore index 8360478..9337953 100644 --- a/.clang-format-ignore +++ b/.clang-format-ignore @@ -1 +1,4 @@ externals/* +faust-src/faust2header.cpp +src/*dsp.h + diff --git a/CMakeLists.txt b/CMakeLists.txt index 40cae87..3fb5738 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,15 +1,18 @@ cmake_minimum_required(VERSION 3.12) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13) +set(CMAKE_OSX_ARCHITECTURES arm64;x86_64) project(QJackTrip) set(nogui FALSE) set(rtaudio TRUE) set(weakjack TRUE) -add_compile_definitions(NO_JTVS) +set(novs FALSE) +add_compile_definitions(NO_UPDATER) add_compile_definitions(QT_OPENSOURCE) if (nogui) add_compile_definitions(NO_GUI) + set(novs TRUE) endif () if (rtaudio) @@ -21,6 +24,10 @@ if (weakjack) include_directories("externals/weakjack") endif() +if (novs) + add_compile_definitions(NO_VS) +endif () + if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set (ENV{PKG_CONFIG_PATH} "/usr/local/lib/pkgconfig") elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows") @@ -43,7 +50,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin find_package(PkgConfig REQUIRED) pkg_check_modules(JACK REQUIRED IMPORTED_TARGET jack) if (weakjack) - # On mac, weakjack doesnt't find jack unless this is explicitly included. + # On mac, weakjack doesn't find jack unless this is explicitly included. include_directories(${JACK_INCLUDE_DIRS}) endif () if (rtaudio) @@ -51,6 +58,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin endif () endif () +set_property(SOURCE src/Regulator.h PROPERTY SKIP_AUTOGEN ON) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. @@ -63,6 +71,10 @@ set(CMAKE_AUTORCC ON) # Find the QtWidgets library if (NOT nogui) find_package(Qt5Widgets CONFIG REQUIRED) + if (NOT novs) + find_package(Qt5Quick CONFIG REQUIRED) + find_package(Qt5NetworkAuth CONFIG REQUIRED) + endif () endif () find_package(Qt5Network CONFIG REQUIRED) @@ -110,8 +122,18 @@ if (NOT nogui) src/gui/about.cpp src/gui/messageDialog.cpp src/gui/textbuf.cpp - src/gui/qjacktrip.qrc ) + + if (NOT novs) + set (qjacktrip_SRC ${qjacktrip_SRC} + src/gui/virtualstudio.cpp + src/gui/vsServerInfo.cpp + src/gui/vsQuickView.cpp + src/gui/qjacktrip.qrc + ) + else () + set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/qjacktrip_novs.qrc) + endif () if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") set (qjacktrip_SRC ${qjacktrip_SRC} win/qjacktrip.rc) @@ -128,9 +150,12 @@ add_compile_definitions(WAIRTOHUB) add_executable(jacktrip ${qjacktrip_SRC}) # Set our libraries for our linker -set (qjacktrip_LIBS Qt5::Widgets) +set (qjacktrip_LIBS Qt5::Network) if (NOT nogui) - set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Network) + set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Widgets) + if (NOT novs) + set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Quick Qt5::NetworkAuth) + endif () endif () if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin") diff --git a/build b/build index 1f8d5ca..b4141be 100755 --- a/build +++ b/build @@ -11,13 +11,15 @@ RTAUDIO=0 NO_SYSTEM_RTAUDIO=0 PRO_FILE="../jacktrip.pro" HELP_STR="usage:\n -./build [noclean nojack rtaudio nogui static install [-config static]]\n\n +./build [noclean nojack rtaudio nogui novs static install [-config static]]\n\n options:\n noclean - do not run \"make clean\" first\n nojack - build without jack\n rtaudio - build with RtAudio\n no-system-rtaudio - use bundled RtAudio library even if it's available in the system\n nogui - build without the gui\n +novs - build without Virtual Studio support\n +noupdater - build without auto-update support\n static - build with static libraries\n weakjack - build with weak linking of jack libraries\n install - install jacktrip in system location (uses sudo)\n @@ -40,6 +42,14 @@ while [[ "$#" -gt 0 ]]; do echo "Building without the gui" CONFIG="-config nogui $CONFIG" ;; + novs) + echo "Building without Virtual Studio support" + CONFIG="-config novs $CONFIG" + ;; + noupdater) + echo "Building without auto-update support" + CONFIG="-config noupdater $CONFIG" + ;; static) echo "Building with static libraries" CONFIG="-config static $CONFIG" @@ -66,6 +76,9 @@ while [[ "$#" -gt 0 ]]; do fi done +echo "All build options:" +echo "$CONFIG" + # Check for Platform platform='unknown' unamestr=`uname` @@ -99,6 +112,8 @@ elif [[ $platform == 'macosx' ]]; then QSPEC=macx-clang-arm64 fi # if qmake is not in path, try homebrew qt5 + echo "path to qmake" + echo "$(which qmake)" if ! command -v $QCMD &> /dev/null; then # echo "qmake not found in \$PATH, searching for homebrew qt5" QT_PREFIX=`brew --prefix qt5` diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 698c760..b722e21 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -16,7 +16,7 @@ Optional: ### Fedora ```sh -dnf install qt5-devel +dnf install qt5-qtbase-devel qt5-qtnetworkauth-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 +apt install qjackctl qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev qtdeclarative5-dev qml-module-qtquick-controls apt install librtaudio-dev # if building with RtAudio ``` diff --git a/docs/Build/Mac.md b/docs/Build/Mac.md index 69c0428..898361f 100644 --- a/docs/Build/Mac.md +++ b/docs/Build/Mac.md @@ -72,7 +72,7 @@ To build using QtCreator: * Open jacktrip.pro using QtCreator * Choose a correctly configured Kit -QtCreator places the `jacktrip` executabe by default in a folder +QtCreator places the `jacktrip` executable by default in a folder with a name like `build-jacktrip-Desktop_x86_darwin_generic_mach_o_64bit-Release/`. ## Installation diff --git a/docs/Build/Meson_build.md b/docs/Build/Meson_build.md index 3ece861..5ea4037 100644 --- a/docs/Build/Meson_build.md +++ b/docs/Build/Meson_build.md @@ -20,7 +20,7 @@ find its documentation at [mesonbuild.com](https://mesonbuild.com/). === "MacOS" ```bash - brew install meson qt5 rt-audio help2man + brew install meson qt5 rtaudio help2man ``` You also need to install Jack, unless you want to disable jack support @@ -47,7 +47,7 @@ Meson shows you also the options of subprojects like RtAudio. ## Build Meson builds in a separate directory. It doesn't touch anything of your project. -This way you can have seperate debug and release build directories for example. +This way you can have separate debug and release build directories for example. Prepare your build directory: ```bash diff --git a/docs/changelog.yml b/docs/changelog.yml index d0d6023..be39c91 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,12 @@ +- Version: "1.6.0" + Date: 2022-05-30 + Description: + - (added) Virtual Studio integration; previous GUI is now called "Classic Mode" + - (added) dblsqd for auto-updates + - (updated) buffer strategy 3 - multiple updates and fixes, still experimental + - (added) JackTrip Labs signing scripts + - (fixed) OpenSSL in the build script + - (updated) code cleanup and maintenance - Version: "1.5.3" Date: 2022-03-28 Description: diff --git a/faust-src/faust2header.cpp b/faust-src/faust2header.cpp index 8c7259f..6b3a647 100644 --- a/faust-src/faust2header.cpp +++ b/faust-src/faust2header.cpp @@ -14,9 +14,8 @@ //---------------------------------------------------------------------------- // FAUST Generated Code //---------------------------------------------------------------------------- -// clang-format off + <> - <> +<> - // clang-format on diff --git a/faust-src/zitarevdsp.dsp b/faust-src/zitarevdsp.dsp index b98a83c..d709c6d 100644 --- a/faust-src/zitarevdsp.dsp +++ b/faust-src/zitarevdsp.dsp @@ -4,6 +4,9 @@ import("stdfaust.lib"); process = zita_rev1; // same as dm.zita_rev1 but for wetness control and some defaults +//process = zita_rev1 : _,attach(cout); // Not using this solution yet, but it works +//cout = ffunction (int cout(), , ""); // dummy function to force #include in output + //----------------------------------`(dm.)zita_rev1`------------------------------ // Example GUI for `zita_rev1_stereo` (mostly following the Linux `zita-rev1` GUI). // diff --git a/jacktrip.pro b/jacktrip.pro index 0b2a5ee..106e37d 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -17,7 +17,7 @@ CONFIG(debug, debug|release) { } equals(QT_EDITION, "OpenSource") { - DEFINES += QT_OPENSOURCE + DEFINES += QT_OPENSOURCE } nogui { @@ -26,6 +26,17 @@ nogui { } else { QT += gui QT += widgets + novs { + DEFINES += NO_VS + } else { + QT += networkauth + QT += qml + QT += quick + QT += svg + } + noupdater { + DEFINES += NO_UPDATER + } } QT += network @@ -44,9 +55,6 @@ nojack { DEFINES += NO_JACK } -# for plugins -INCLUDEPATH += faust-src-lair/stk - !win32 { INCLUDEPATH+=/usr/local/include # wair needs stk, can be had from linux this way @@ -140,7 +148,6 @@ linux-g++-64 { message(Linux 64bit) } - win32 { message(Building on win32) #cc CONFIG += x86 console @@ -184,14 +191,11 @@ QMAKE_CLEAN += -r ./jacktrip ./jacktrip_debug ./release/* ./debug/* ./$${applica # isEmpty(PREFIX) will allow path to be changed during the command line # call to qmake, e.g. qmake PREFIX=/usr isEmpty(PREFIX) { - PREFIX = /usr/local + PREFIX = /usr/local } target.path = $$PREFIX/bin/ INSTALLS += target -# for plugins -INCLUDEPATH += faust-src-lair - # Input HEADERS += src/DataProtocol.h \ src/JackTrip.h \ @@ -223,20 +227,31 @@ HEADERS += src/DataProtocol.h \ #(Removed JackTripThread.h JackTripWorkerMessages.h NetKS.h TestRingBuffer.h ThreadPoolTest.h) !nojack { -HEADERS += src/JackAudioInterface.h \ - src/JMess.h \ - src/Patcher.h + HEADERS += src/JackAudioInterface.h \ + src/JMess.h \ + src/Patcher.h } !nogui { -HEADERS += src/gui/about.h \ - src/gui/messageDialog.h \ - src/gui/qjacktrip.h \ - src/gui/textbuf.h + HEADERS += src/gui/about.h \ + src/gui/messageDialog.h \ + src/gui/qjacktrip.h \ + src/gui/textbuf.h + !novs { + HEADERS += src/gui/virtualstudio.h \ + src/gui/vsServerInfo.h \ + src/gui/vsQuickView.h + } + !noupdater { + HEADERS += src/dblsqd/feed.h \ + src/dblsqd/release.h \ + src/dblsqd/semver.h \ + src/dblsqd/update_dialog.h + } } rtaudio|bundled_rtaudio { - HEADERS += src/RtAudioInterface.h + HEADERS += src/RtAudioInterface.h } SOURCES += src/DataProtocol.cpp \ @@ -262,16 +277,27 @@ SOURCES += src/DataProtocol.cpp \ #(Removed jacktrip_main.cpp jacktrip_tests.cpp JackTripThread.cpp ProcessPlugin.cpp) !nojack { -SOURCES += src/JackAudioInterface.cpp \ - src/JMess.cpp \ - src/Patcher.cpp + SOURCES += src/JackAudioInterface.cpp \ + src/JMess.cpp \ + src/Patcher.cpp } !nogui { -SOURCES += src/gui/messageDialog.cpp \ - src/gui/qjacktrip.cpp \ - src/gui/about.cpp \ - src/gui/textbuf.cpp + SOURCES += src/gui/messageDialog.cpp \ + src/gui/qjacktrip.cpp \ + src/gui/about.cpp \ + src/gui/textbuf.cpp + !novs { + SOURCES += src/gui/virtualstudio.cpp \ + src/gui/vsServerInfo.cpp \ + src/gui/vsQuickView.cpp + } + !noupdater { + SOURCES += src/dblsqd/feed.cpp \ + src/dblsqd/release.cpp \ + src/dblsqd/semver.cpp \ + src/dblsqd/update_dialog.cpp + } } !nogui { @@ -279,13 +305,19 @@ SOURCES += src/gui/messageDialog.cpp \ HEADERS += src/gui/NoNap.h OBJECTIVE_SOURCES += src/gui/NoNap.mm } - FORMS += src/gui/qjacktrip.ui src/gui/about.ui src/gui/messageDialog.ui - RESOURCES += src/gui/qjacktrip.qrc + novs { + RESOURCES += src/gui/qjacktrip_novs.qrc + } else { + RESOURCES += src/gui/qjacktrip.qrc + } + !noupdater { + FORMS += src/dblsqd/update_dialog.ui + } } rtaudio|bundled_rtaudio { - SOURCES += src/RtAudioInterface.cpp + SOURCES += src/RtAudioInterface.cpp } weakjack { diff --git a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml index 8fe947c..ef0cb43 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' +runtime-version: '5.15-21.08' sdk: org.kde.Sdk command: jacktrip finish-args: @@ -9,12 +9,12 @@ finish-args: - --socket=x11 # Wayland access # - --socket=wayland + # OpenGL + - --device=dri # Needs network access - --share=network # Pipewire/Jack - --filesystem=xdg-run/pipewire-0 - # For setting realtime priority for network thread? - - --socket=system-bus cleanup: - /lib/python3.8 - /share/man @@ -27,20 +27,20 @@ modules: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation sources: - type: file - sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz + sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 - name: python3-jinja2 buildsystem: simple cleanup: [ "*" ] build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation sources: - - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a - type: file - url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz - - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 - type: file - url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl + - type: file + url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz + sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b + - type: file + url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl + sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - name: jacktrip buildsystem: meson config-opts: diff --git a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 index 93c7f28..b2d211c 100644 --- a/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 +++ b/linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2 @@ -1,20 +1,20 @@ app-id: org.jacktrip.JackTrip.Devel runtime: org.kde.Platform -runtime-version: '5.15' +runtime-version: '5.15-21.08' sdk: org.kde.Sdk command: jacktrip finish-args: # X11 + XShm access - --share=ipc - --socket=x11 - # Wayland access (disabled because of missing window shadows) + # Wayland access # - --socket=wayland + # OpenGL + - --device=dri # Needs network access - --share=network # Pipewire/Jack - --filesystem=xdg-run/pipewire-0 - # For setting realtime priority for network thread? - - --socket=system-bus cleanup: - /lib/python3.8 - /share/man @@ -27,20 +27,20 @@ modules: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation sources: - type: file - sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz + sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 - name: python3-jinja2 buildsystem: simple cleanup: [ "*" ] build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation sources: - - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a - type: file - url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz - - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 - type: file - url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl + - type: file + url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz + sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b + - type: file + url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl + sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - name: jacktrip buildsystem: meson config-opts: diff --git a/linux/flatpak/org.jacktrip.JackTrip.yml b/linux/flatpak/org.jacktrip.JackTrip.yml index eda8152..9bdfcc5 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' +runtime-version: '5.15-21.08' sdk: org.kde.Sdk command: jacktrip finish-args: @@ -9,12 +9,12 @@ finish-args: - --socket=x11 # Wayland access # - --socket=wayland + # OpenGL + - --device=dri # Needs network access - --share=network # Pipewire/Jack - --filesystem=xdg-run/pipewire-0 - # For setting realtime priority for network thread? - - --socket=system-bus cleanup: - /lib/python3.8 - /share/man @@ -27,20 +27,20 @@ modules: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation sources: - type: file - sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz + sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 - name: python3-jinja2 buildsystem: simple cleanup: [ "*" ] build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation sources: - - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a - type: file - url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz - - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 - type: file - url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl + - type: file + url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz + sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b + - type: file + url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl + sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - name: jacktrip buildsystem: meson sources: diff --git a/macos/JackTrip.app_template/Contents/Info.plist b/macos/JackTrip.app_template/Contents/Info.plist index 58cd87e..0ca37fd 100644 --- a/macos/JackTrip.app_template/Contents/Info.plist +++ b/macos/JackTrip.app_template/Contents/Info.plist @@ -27,7 +27,7 @@ MacOSX CFBundleVersion - 16 + %VERSION% DTCompiler com.apple.compilers.llvm.clang.1_0 DTPlatformBuild diff --git a/macos/assemble_app.sh b/macos/assemble_app.sh index aa7c4b4..6318bb7 100755 --- a/macos/assemble_app.sh +++ b/macos/assemble_app.sh @@ -47,7 +47,7 @@ while getopts ":inhc:d:u:p:a:b:" opt; do h) echo "JackTrip App Bundle assembly script." echo "Copyright (C) 2020-2021 Aaron Wyatt et al." - echo "Relased under the GNU GPLv3 License." + echo "Released under the GNU GPLv3 License." echo echo "Usage: ./assemble-app.sh [options] [appname] [bundlename]" echo @@ -114,10 +114,15 @@ if [ ! -z "$DYNAMIC_QT" ]; then exit 1 fi fi + VS=$(otool -L ../builddir/jacktrip | grep QtQml) + QMLDIR="" + if [ ! -z "VS" ]; then + QMLDIR=" -qmldir=../src/gui" + fi if [ ! -z "$CERTIFICATE" ]; then - $DEPLOY_CMD "$APPNAME.app" -codesign="$CERTIFICATE" + $DEPLOY_CMD "$APPNAME.app"$QMLDIR -codesign="$CERTIFICATE" else - $DEPLOY_CMD "$APPNAME.app" + $DEPLOY_CMD "$APPNAME.app"$QMLDIR fi fi diff --git a/macos/package/JackTrip.pkgproj_template b/macos/package/JackTrip.pkgproj_template index f5f2af4..18a6e56 100644 --- a/macos/package/JackTrip.pkgproj_template +++ b/macos/package/JackTrip.pkgproj_template @@ -42,7 +42,7 @@ BUNDLE_POSTINSTALL_PATH PATH - link.sh + postinstall.sh PATH_TYPE 1 diff --git a/macos/package/link.sh b/macos/package/link.sh deleted file mode 100644 index 350da28..0000000 --- a/macos/package/link.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -mkdir -p /usr/local/bin -rm -f /usr/local/bin/jacktrip -ln -s "$2"/Contents/MacOS/jacktrip /usr/local/bin/jacktrip diff --git a/macos/package/postinstall.sh b/macos/package/postinstall.sh new file mode 100755 index 0000000..a668d3c --- /dev/null +++ b/macos/package/postinstall.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Link jacktrip to app binary +mkdir -p /usr/local/bin +rm -f /usr/local/bin/jacktrip +ln -s "$2"/Contents/MacOS/jacktrip /usr/local/bin/jacktrip + +# Open JackTrip on intaller finish +open -a /Applications/JackTrip.app +exit 0 diff --git a/meson.build b/meson.build index 4293f56..4c36da9 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jacktrip', ['cpp','c'], - default_options: ['cpp_std=c++17','warning_level=2']) + default_options: ['cpp_std=c++17','warning_level=2','optimization=2']) if get_option('profile') == 'development' application_id = 'org.jacktrip.JackTrip.Devel' @@ -80,19 +80,60 @@ if get_option('nogui') == true defines += '-DNO_GUI' qt5_dep = dependency('qt5', modules: ['Core', 'Network'], include_type: 'system') else - qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') - src += ['src/gui/qjacktrip.cpp', + src += [ + 'src/gui/qjacktrip.cpp', 'src/gui/about.cpp', 'src/gui/messageDialog.cpp', - 'src/gui/textbuf.cpp'] - ui_h += ['src/gui/qjacktrip.ui', - 'src/gui/messageDialog.ui', - 'src/gui/about.ui'] - moc_h += ['src/gui/about.h', + 'src/gui/textbuf.cpp' + ] + moc_h += [ + 'src/gui/about.h', 'src/gui/qjacktrip.h', 'src/gui/messageDialog.h', - 'src/gui/textbuf.h'] - qres = ['src/gui/qjacktrip.qrc'] + 'src/gui/textbuf.h' + ] + ui_h += [ + 'src/gui/qjacktrip.ui', + 'src/gui/messageDialog.ui', + 'src/gui/about.ui' + ] + + if get_option('novs') == true + defines += '-DNO_VS' + qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') + qres = ['src/gui/qjacktrip_novs.qrc'] + else + src += [ + 'src/gui/virtualstudio.cpp', + 'src/gui/vsServerInfo.cpp', + 'src/gui/vsQuickView.cpp' + ] + moc_h += [ + 'src/gui/virtualstudio.h', + 'src/gui/vsServerInfo.h', + 'src/gui/vsQuickView.h' + ] + qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth'], include_type: 'system') + qres = ['src/gui/qjacktrip.qrc'] + endif + + if get_option('noupdater') == true + defines += '-DNO_UPDATER' + else + src += [ + 'src/dblsqd/feed.cpp', + 'src/dblsqd/release.cpp', + 'src/dblsqd/semver.cpp', + 'src/dblsqd/update_dialog.cpp' + ] + moc_h += [ + 'src/dblsqd/feed.h', + 'src/dblsqd/release.h', + 'src/dblsqd/semver.h', + 'src/dblsqd/update_dialog.h' + ] + ui_h += ['src/dblsqd/update_dialog.ui'] + endif endif deps += qt5_dep diff --git a/meson_options.txt b/meson_options.txt index 6d93cd7..242ea80 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,4 +3,6 @@ option('rtaudio', type : 'feature', value : 'auto', description: 'Build with RtA option('jack', type : 'feature', value : 'auto', description: 'Build with JACK Backend') option('weakjack', type : 'boolean', value : 'false', description: 'Weak link JACK library') option('nogui', type : 'boolean', value : 'false', description: 'Build without graphical user interface') +option('novs', type : 'boolean', value : 'false', description: 'Build without Virtual Studio support') +option('noupdater', type : 'boolean', value : 'false', description: 'Build without auto-update support') option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly') diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json new file mode 100644 index 0000000..97de59d --- /dev/null +++ b/releases/edge/mac-manifests.json @@ -0,0 +1,65 @@ +{ + "app_name": "JackTrip", + "releases": [ + { + "version": "1.6.0", + "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", + "download": { + "date": "2022-06-01T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-macOS-x64-installer.pkg", + "downloadSize": 11474299, + "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4" + } + }, + { + "version": "1.6.0-rc.5", + "changelog": "Release candidate 5 for 1.6.0", + "download": { + "date": "2022-05-30T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-macOS-x64-installer.pkg", + "downloadSize": 11474262, + "sha256": "8289530a8e6ef1f772776c7078679e2dac146f366cfc4e8c09e0ad16865fe274" + } + }, + { + "version": "1.6.0-rc.4", + "changelog": "Release candidate 4 for 1.6.0", + "download": { + "date": "2022-05-29T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-macOS-x64-installer.pkg", + "downloadSize": 11460550, + "sha256": "38d817f3e8cc61b707392ce74cee8ab46da9c8eb2086ea2b3f0c79496caf70a8" + } + }, + { + "version": "1.6.0-rc.3", + "changelog": "Release candidate 3 for 1.6.0", + "download": { + "date": "2022-05-27T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-macOS-x64-installer.pkg", + "downloadSize": 11460230, + "sha256": "c9614964974d61c062d905f01c7d30ab04a697562ecfba6264392aebe7161051" + } + }, + { + "version": "1.6.0-rc.2", + "changelog": "Release candidate 2 for 1.6.0", + "download": { + "date": "2022-05-26T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-macOS-x64-signed-installer.pkg", + "downloadSize": 11460155, + "sha256": "ad508680115f73036da3a5328ddf0841b86620406406e0ffaa4b982e24a27771" + } + }, + { + "version": "1.6.0-rc.1", + "changelog": "Release candidate 1 for 1.6.0", + "download": { + "date": "2022-05-23T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-macOS-x64-installer.pkg", + "downloadSize": 11076221, + "sha256": "071cda0ce59361e474a04db00beec41e92d2d823dab71e3fab179faf89f6fd7e" + } + } + ] +} \ No newline at end of file diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json new file mode 100644 index 0000000..59b8186 --- /dev/null +++ b/releases/edge/win-manifests.json @@ -0,0 +1,65 @@ +{ + "app_name": "JackTrip", + "releases": [ + { + "version": "1.6.0", + "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", + "download": { + "date": "2022-06-01T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-Windows-x64-installer.msi", + "downloadSize": 43364352, + "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078" + } + }, + { + "version": "1.6.0-rc.5", + "changelog": "Release candidate 5 for 1.6.0", + "download": { + "date": "2022-05-30T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-Windows-x64-installer.msi", + "downloadSize": 43364352, + "sha256": "d84e6e5d21cf31f5dd48e9dcc0c1a44fe7a37d977f94b6ff63d5e381745e5a44" + } + }, + { + "version": "1.6.0-rc.4", + "changelog": "Release candidate 4 for 1.6.0", + "download": { + "date": "2022-05-29T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-Windows-x64-installer.msi", + "downloadSize": 43126784, + "sha256": "cdb0ef906cf0d6047289838bf013b31a626cdd74dd4f75d6c1c4c3adbc9cd41d" + } + }, + { + "version": "1.6.0-rc.3", + "changelog": "Release candidate 3 for 1.6.0", + "download": { + "date": "2022-05-27T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-Windows-x64-installer.msi", + "downloadSize": 43118592, + "sha256": "dceaf670a67cf1541007db82c5ce937b25370a7140e48192b94470f575fc4988" + } + }, + { + "version": "1.6.0-rc.2", + "changelog": "Release candidate 2 for 1.6.0", + "download": { + "date": "2022-05-26T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-Windows-x64-signed-installer.msi", + "downloadSize": 43114496, + "sha256": "b1a7adc8dc0fb47f59515790e8531dd10838d799bacb4b5653192ed621bca208" + } + }, + { + "version": "1.6.0-rc.1", + "changelog": "Release candidate 1 for 1.6.0", + "download": { + "date": "2022-05-23T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-Windows-x64-installer.msi", + "downloadSize": 43081728, + "sha256": "240f8b495ec5057228be922da80829a3718b474bafc2ba2d77750643abd1005c" + } + } + ] +} \ No newline at end of file diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json new file mode 100644 index 0000000..0faec7c --- /dev/null +++ b/releases/stable/mac-manifests.json @@ -0,0 +1,15 @@ +{ + "app_name": "JackTrip", + "releases": [ + { + "version": "1.6.0", + "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", + "download": { + "date": "2022-06-01T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-macOS-x64-installer.pkg", + "downloadSize": 11474299, + "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4" + } + } + ] +} \ No newline at end of file diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json new file mode 100644 index 0000000..cb255c0 --- /dev/null +++ b/releases/stable/win-manifests.json @@ -0,0 +1,15 @@ +{ + "app_name": "JackTrip", + "releases": [ + { + "version": "1.6.0", + "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", + "download": { + "date": "2022-06-01T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-Windows-x64-installer.msi", + "downloadSize": 43364352, + "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078" + } + } + ] +} \ No newline at end of file diff --git a/scripts/hubMode/test_hub_mode_server_and_client.sh b/scripts/hubMode/test_hub_mode_server_and_client.sh index ff49980..3c3fc1d 100755 --- a/scripts/hubMode/test_hub_mode_server_and_client.sh +++ b/scripts/hubMode/test_hub_mode_server_and_client.sh @@ -48,7 +48,7 @@ if [ $JACKD != 0 ] # killall jackd if [ "$(ps -aux | grep -c jackd)" != 1 ]; then killall jackd; fi; # if jackd is or has been running with another driver -# much experimenation shows it literally takes this long +# much experimentation shows it literally takes this long sleep 17 # to flush old connections before starting the dummy driver diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index ba65477..a6dc55b 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -160,7 +160,7 @@ void AudioInterface::setup() mAudioInputPacket = new int8_t[size_audio_input]; mAudioOutputPacket = new int8_t[size_audio_output]; - // Initialize and asign memory for ProcessPlugins Buffers + // Initialize and assign memory for ProcessPlugins Buffers #ifdef WAIR // WAIR if (mNumNetRevChans) { mInProcessBuffer.resize(mNumNetRevChans); diff --git a/src/AudioInterface.h b/src/AudioInterface.h index db28f42..293e84a 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -109,9 +109,9 @@ class AudioInterface /** \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 - * is reponsible to check that each channel has n_frames samplers + * is responsible to check that each channel has n_frames samplers * \param in_buffer Array of output audio samplers for each channel. The user - * is reponsible to check that each channel has n_frames samplers + * is responsible to check that each channel has n_frames samplers */ virtual void broadcastCallback(QVarLengthArray& mon_buffer, unsigned int n_frames); diff --git a/src/Auth.cpp b/src/Auth.cpp index 3cbd351..40eaeef 100644 --- a/src/Auth.cpp +++ b/src/Auth.cpp @@ -112,7 +112,7 @@ void Auth::loadAuthFile(const QString& filename) continue; } - // Check that our password hash is useable. + // Check that our password hash is usable. bool invalid = false; if (lineParts.at(1).startsWith(QLatin1String("$6$"))) { QStringList hashParts = lineParts.at(1).split(QStringLiteral("$")); @@ -185,7 +185,7 @@ bool Auth::checkTime(const QString& username) char Auth::char64(int value) { - // Returns a base 64 enconding using the following characters: + // Returns a base 64 encoding using the following characters: // ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz if (value < 0 || value >= 64) { return 0; diff --git a/src/Compressor.cpp b/src/Compressor.cpp index 954a749..e0cb324 100644 --- a/src/Compressor.cpp +++ b/src/Compressor.cpp @@ -37,8 +37,6 @@ #include "Compressor.h" -#include - //******************************************************************************* void Compressor::compute(int nframes, float** inputs, float** outputs) { diff --git a/src/Compressor.h b/src/Compressor.h index 1208779..84fe58e 100644 --- a/src/Compressor.h +++ b/src/Compressor.h @@ -41,6 +41,7 @@ #ifndef __COMPRESSOR_H__ #define __COMPRESSOR_H__ +#include #include #include "CompressorPresets.h" diff --git a/src/DataProtocol.h b/src/DataProtocol.h index b01643f..38755b6 100644 --- a/src/DataProtocol.h +++ b/src/DataProtocol.h @@ -153,7 +153,7 @@ class DataProtocol : public QThread */ virtual void setPeerAddress(const char* peerHostOrIP) = 0; - /** \brief Set the peer incomming (receiving) port number + /** \brief Set the peer incoming (receiving) port number * \param port Port number * \todo implement here instead of in the subclass UDP */ diff --git a/src/JMess.cpp b/src/JMess.cpp index 85c6029..8517334 100644 --- a/src/JMess.cpp +++ b/src/JMess.cpp @@ -150,7 +150,7 @@ void JMess::connectTUB(int /*nChans*/) } // SC to jacktrip - tmp += 4; // increase tmp for port offest + tmp += 4; // increase tmp for port offset qDebug() << "connect " << serverAudio << HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT << tmp << "with " << client << ":send_" << l; diff --git a/src/JMess.h b/src/JMess.h index afb78c3..08b602b 100644 --- a/src/JMess.h +++ b/src/JMess.h @@ -50,7 +50,7 @@ const int Indent = 2; /*! \brief Class to save and load all jack client connections. * * Saves an XML file with all the current jack connections. This same file can - * be loaded to connect evrything again. The XML file can also be edited. + * be loaded to connect everything again. The XML file can also be edited. * * Has also an option to disconnect all the clients. */ diff --git a/src/JackAudioInterface.h b/src/JackAudioInterface.h index 44884f1..a3f6633 100644 --- a/src/JackAudioInterface.h +++ b/src/JackAudioInterface.h @@ -165,8 +165,8 @@ class JackAudioInterface : public AudioInterface * * jack_set_process_callback needs a static member function pointer. A normal * member function won't work because a this pointer is passed under the - * scenes. That's why we need to cast the member funcion processCallback to the static - * function wrapperProcessCallback. The callback is then set as:\n + * scenes. That's why we need to cast the member function processCallback to the + * static function wrapperProcessCallback. The callback is then set as:\n * jack_set_process_callback(mClient, JackAudioInterface::wrapperProcessCallback, * this) */ diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index e841731..a5475bb 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -168,8 +168,8 @@ void JackTrip::setupAudio( { // Check if mAudioInterface has already been created or not if (mAudioInterface - != NULL) { // if it has been created, disconnet it from JACK and delete it - cout << "WARINING: JackAudio interface was setup already:" << endl; + != NULL) { // if it has been created, disconnect it from JACK and delete it + cout << "WARNING: JackAudio interface was setup already:" << endl; cout << "It will be erased and setup again." << endl; cout << gPrintSeparator << endl; closeAudio(); @@ -377,15 +377,20 @@ void JackTrip::setupRingBuffers() new RingBuffer(audio_output_slot_size, mBufferQueueLength); mPacketHeader->setBufferRequiresSameSettings(true); } else if (mBufferStrategy == 3) { - qDebug() << "experimental buffer strategy 3 -- regulator with PLC"; - mSendRingBuffer = - new RingBuffer(audio_input_slot_size, gDefaultOutputQueueLength); + cout << "Using experimental buffer strategy " << mBufferStrategy + << "-- Regulator with PLC" << endl; + mReceiveRingBuffer = - new Regulator(mSampleRate, mNumAudioChansOut, mAudioBitResolution, - mAudioBufferSize, mBufferQueueLength); + new Regulator(mNumAudioChansOut, mAudioBitResolution, mAudioBufferSize, + mBufferQueueLength, mBroadcastQueueLength); // bufStrategy 3, mBufferQueueLength is in integer msec not packets mPacketHeader->setBufferRequiresSameSettings(false); // = asym is default + + if (0 < mBroadcastQueueLength) { + mAudioInterface->enableBroadcastOutput(); + } + } else { cout << "Using JitterBuffer strategy " << mBufferStrategy << endl; if (0 > mBufferQueueLength) { @@ -522,7 +527,7 @@ void JackTrip::startProcess( "clientPingToServerStart" << std::endl; if (clientPingToServerStart() - == -1) { // if error on server start (-1) we return inmediatly + == -1) { // if error on server start (-1) we return immediately stop(QStringLiteral( "Peer Address has to be set if you run in CLIENTTOPINGSERVER mode")); return; @@ -536,7 +541,7 @@ void JackTrip::startProcess( << " JackTrip:startProcess case SERVERPINGSERVER before serverStart" << std::endl; if (serverStart(true) - == -1) { // if error on server start (-1) we return inmediatly + == -1) { // if error on server start (-1) we return immediately stop(); return; } @@ -651,8 +656,7 @@ void JackTrip::onStatTimer() << now.toLocal8Bit().constData() << " " << getPeerAddress().toLocal8Bit().constData() << " send: " << send_io_stat.underruns << "/" << send_io_stat.overflows - << " Pull underruns: " - << recv_io_stat.underruns // pullStat->lastPlcUnderruns; + << " Glitches: " << recv_io_stat.underruns // pullStat->lastPlcUnderruns; #define INVFLOATFACTOR 0.001 << "\nPUSH -- SD avg/last: " << setw(5) << INVFLOATFACTOR * recv_io_stat.overflows // pushStat->longTermStdDev; @@ -679,8 +683,9 @@ void JackTrip::onStatTimer() // << "/" << recv_io_stat.overflows << " prot: " << // pkt_stat.lost << "/" // << pkt_stat.outOfOrder << "/" << pkt_stat.revived - << " \n tot: " - << pkt_stat.tot + << " \n tot: " << pkt_stat.tot << " \t tol: " << setw(5) + << INVFLOATFACTOR * recv_io_stat.autoq_corr << " \t dsp (last): " << setw(5) + << INVFLOATFACTOR * recv_io_stat.autoq_rate // << " sync: " << recv_io_stat.level << "/" // << recv_io_stat.buf_inc_underrun << "/" // << recv_io_stat.buf_inc_compensate << "/" @@ -826,7 +831,7 @@ void JackTrip::receivedDataTCP() mTcpClient.close(); // Close the socket // cout << "TCP Socket Closed!" << endl; - // If we sent authentication data, check if our authentication attempt was succesfull + // If we sent authentication data, check if our authentication attempt was successful if (mUseAuth && udp_port > 65535) { QString error_message; if (udp_port == Auth::WRONGCREDS) { @@ -854,7 +859,7 @@ void JackTrip::receivedDataTCP() } if (gVerboseFlag) - cout << "Connection Succesfull!" << endl; + cout << "Connection Successful!" << endl; // Set with the received UDP port // ------------------------------ @@ -967,7 +972,7 @@ void JackTrip::receivedDataUDP() // We reply to the same port the peer sent the packets from // This way we can go through NAT // Because of the NAT traversal scheme, the portn need to be - // "symetric", e.g.: + // "symmetric", e.g.: // from Client to Server : src = 4474, dest = 4464 // from Server to Client : src = 4464, dest = 4474 // no -- all are the same -- 4464 @@ -1116,6 +1121,7 @@ int JackTrip::serverStart(bool timeout, int udpTimeout) // udpTimeout unused mEndTime = udpTimeout; } mTimeoutTimer.setInterval(mSleepTime); + mTimeoutTimer.disconnect(); connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTrip::udpTimerTick); mTimeoutTimer.start(); } @@ -1210,6 +1216,7 @@ int JackTrip::clientPingToServerStart() mElapsedTime = 0; mEndTime = 5000; // Timeout after 5 seconds. mTimeoutTimer.setInterval(mSleepTime); + mTimeoutTimer.disconnect(); connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTrip::tcpTimerTick); mTimeoutTimer.start(); } @@ -1297,7 +1304,7 @@ active address local_addr.sin_port = htons(bind_port); //set bind port ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); #endif #if defined ( __APPLE__ ) - // This option is not avialable on Linux, and without it MAC OS X + // This option is not available on Linux, and without it MAC OS X // has problems rebinding a socket ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); #endif diff --git a/src/JackTrip.h b/src/JackTrip.h index 0c2ec25..fa877d0 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -399,6 +399,12 @@ class JackTrip : public QObject { return mReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen); } + virtual bool writeAudioBufferRegulator(const int8_t* ptrToSlot, int len, int seq, + int lostLen) + { + return mReceiveRingBuffer->insertSlotNonBlockingRegulator(ptrToSlot, len, seq, + lostLen); + } uint32_t getBufferSizeInSamples() const { return mAudioBufferSize; /*return mAudioInterface->getBufferSizeInSamples();*/ @@ -597,8 +603,8 @@ class JackTrip : public QObject /// \brief Starts for the CLIENT mode void clientStart(); /// \brief Starts for the SERVER mode - /// \param timout Set the server to timeout after 2 seconds if no client connections - /// are received. Usefull for the multithreaded server \return 0 on success, -1 on + /// \param timeout Set the server to timeout after 2 seconds if no client connections + /// are received. Useful for the multithreaded server \return 0 on success, -1 on /// error int serverStart(bool timeout = false, int udpTimeout = gTimeOutMultiThreadedServer); /// \brief Stats for the Client to Ping Server diff --git a/src/JackTripWorker.h b/src/JackTripWorker.h index 18a8127..7ce1d9e 100644 --- a/src/JackTripWorker.h +++ b/src/JackTripWorker.h @@ -152,10 +152,10 @@ class JackTripWorker : public QObject UdpHubListener* mUdpHubListener; ///< Hub Listener Socket // QHostAddress mClientAddress; ///< Client Address QString mClientAddress; - uint16_t mServerPort; ///< Server Ephemeral Incomming Port to use with Client + uint16_t mServerPort; ///< Server Ephemeral Incoming Port to use with Client bool m_connectDefaultAudioPorts = false; - /// Client Outgoing Port. By convention, the receving port will be mClientPort + /// Client Outgoing Port. By convention, the receiving port will be mClientPort /// -1 uint16_t mClientPort; diff --git a/src/Limiter.cpp b/src/Limiter.cpp index 80213f7..b23b5f0 100644 --- a/src/Limiter.cpp +++ b/src/Limiter.cpp @@ -38,8 +38,6 @@ #include "Limiter.h" -#include - #include "jacktrip_types.h" //******************************************************************************* diff --git a/src/Limiter.h b/src/Limiter.h index f311308..fea43bd 100644 --- a/src/Limiter.h +++ b/src/Limiter.h @@ -49,6 +49,7 @@ #endif #include +#include #include #include "ProcessPlugin.h" diff --git a/src/PacketHeader.cpp b/src/PacketHeader.cpp index cbd3bdf..23011e7 100644 --- a/src/PacketHeader.cpp +++ b/src/PacketHeader.cpp @@ -297,7 +297,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio() // one channel" // << endl; // std::exit(1); - // std::cerr << "WARINING: JamLink only support ONE channel. Run JackTrip using + // std::cerr << "WARNING: JamLink only support ONE channel. Run JackTrip using // only one channel" << endl; throw std::logic_error("JamLink only support ONE // channel. Run JackTrip using only one channel"); emit signalError(QStringLiteral( @@ -307,7 +307,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio() // Sampling Rate int rate_type = mJackTrip->getSampleRateType(); if (rate_type != AudioInterface::SR48) { - // std::cerr << "WARINING: JamLink only support 48kHz for communication with + // std::cerr << "WARNING: JamLink only support 48kHz for communication with // JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink only // support 48kHz for communication with JackTrip at the moment."); emit signalError( @@ -318,7 +318,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio() // Check Buffer Size int buf_size = mJackTrip->getBufferSizeInSamples(); if (buf_size != 64) { - // std::cerr << "WARINING: JamLink only support 64 buffer size for communication + // std::cerr << "WARNING: JamLink only support 64 buffer size for communication // with JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink // only support 64 buffer size for communication with JackTrip at the moment."); emit signalError(QStringLiteral( diff --git a/src/PacketHeader.h b/src/PacketHeader.h index 30e9480..c31b4ba 100644 --- a/src/PacketHeader.h +++ b/src/PacketHeader.h @@ -48,7 +48,7 @@ #include "jacktrip_types.h" class JackTrip; // Forward Declaration -/// \brief Abstract Header Struct, Header Stucts should subclass it +/// \brief Abstract Header Struct, Header Structs should subclass it struct HeaderStruct { }; diff --git a/src/Regulator.cpp b/src/Regulator.cpp index a9c8e51..24a6c7d 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -35,30 +35,47 @@ * \date May-Sep 2021 */ -// EXPERIMENTAL for testing in JackTrip v1.5.0 -// requires server and client have same FPP -// runs ok from FPP 16 up to 1024 -// number of in / out channels should be the same -// mono, stereo and -n3 tested fine - -// ./jacktrip -S --udprt -p1 --bufstrategy 3 -I 1 -q10 -// PIPEWIRE_LATENCY=32/48000 ./jacktrip -C --udprt --bufstrategy 3 -I 1 -q4 - +// EXPERIMENTAL for testing in JackTrip v1.5. +// server and client can have different FPP (tested from FPP 16 to 1024) +// stress tested by repeatedly starting & stopping across range of FPP's +// server and client can have different in / out channel count +// large FPP, for example 512, should not be run with --udprt as PLC audio callback +// completion gets delayed auto mode -- use -q auto3 or for manual setting of initial +// mMsecTolerance -- use -q auto gathers data for 6 sec and then goes full auto + +// example WAN test +// ./jacktrip -S --udprt -p1 --bufstrategy 3 -q auto +// PIPEWIRE_LATENCY=32/48000 ./jacktrip -C --udprt --bufstrategy 3 -q auto + +// (mono : mono : mono) +// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 1 +// --udprt -I 1 +// ./jacktrip -C --receivechannels 1 -u --sendchannels 1 --bufstrategy 3 -q auto3 +// -I 1 --udprt + +// latest (mono <: stereo : stereo) +// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 2 +// --udprt -I 1 +// ./jacktrip -C --receivechannels 2 -u --sendchannels 1 --bufstrategy 3 -q auto3 +// -I 1 --udprt + +// example WAN test // at 48000 / 32 = 2.667 ms total roundtrip latency -// local loopback test with 4 terminals running and the following jmess file +// local loopback test with 4 terminals running and a jmess file // jacktrip -S --udprt --nojackportsconnect -q1 --bufstrategy 3 // jacktrip -C localhost --udprt --nojackportsconnect -q1 --bufstrategy 3 -// use jack_iodelay -// use jmess -s delay.xml and jmess -c delay.xml // tested outgoing loss impairments with (replace lo with relevant network interface) -// sudo tc qdisc add dev lo root netem loss 2% -// sudo tc qdisc del dev lo root netem loss 2% +// sudo tc qdisc add dev lo root netem loss 5% +// sudo tc qdisc del dev lo root netem loss 5% +// or very revealing +// sudo tc qdisc add dev lo root netem loss 20% +// sudo tc qdisc del dev lo root netem loss 20% // tested jitter impairments with -// for wifi +// wifi simulation // sudo tc qdisc add dev lo root netem slot distribution pareto 0.1ms 3.0ms // sudo tc qdisc del dev lo root netem slot distribution pareto 0.1ms 3.0ms -// for wired cmn9 +// ugly wired simulation // sudo tc qdisc add dev lo root netem slot distribution pareto 0.2ms 0.3ms // sudo tc qdisc del dev lo root netem slot distribution pareto 0.2ms 0.3ms @@ -67,30 +84,49 @@ #include #include +#include "JitterBuffer.h" #include "jacktrip_globals.h" using std::cout; using std::endl; using std::setw; -// constants... tested for now -constexpr int HIST = 6; // at FPP32 +// constants... +constexpr int HIST = 4; // for mono at FPP 16-128, see below for > mono, > 128 constexpr int ModSeqNumInit = 256; // bounds on seqnums, 65536 is max in packet header constexpr int NumSlotsMax = 128; // mNumSlots looped for recent arrivals constexpr int LostWindowMax = 32; // mLostWindow looped for recent arrivals +constexpr double DefaultAutoHeadroom = + 3.0; // msec padding for auto adjusting mMsecTolerance +constexpr double AutoMax = 250.0; // msec bounds on insane IPI, like ethernet unplugged +constexpr double AutoInitDur = 6000.0; // kick in auto after this many msec +constexpr double AutoInitValFactor = + 0.5; // scale for initial mMsecTolerance during init phase if unspecified +// tweak +constexpr int WindowDivisor = 8; // for faster auto tracking +constexpr int MaxFPP = 1024; // tested up to this FPP //******************************************************************************* -Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qLen) +Regulator::Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen) : RingBuffer(0, 0) - , mNumChannels(channels) + , mNumChannels(rcvChannels) , mAudioBitRes(bit_res) , mFPP(FPP) - , mSampleRate(sample_rate) - , mMsecTolerance((double)qLen) + , mMsecTolerance((double)qLen) // handle non-auto mode, expects positive qLen , mAuto(false) + , m_b_BroadcastQueueLength(bqLen) { - if (mMsecTolerance < 0.0) { // handle, for example, CLI -q auto15 or -q auto - mAuto = true; - mMsecTolerance *= -1.0; - }; + // catch settings that are compute bound using long HIST + // hub client rcvChannels is set from client's settings parameters + // hub server rcvChannels is set from connecting client, not from hub parameters + // if (mNumChannels > MaxChans) { + // std::cerr << "*** Regulator.cpp: receive channels = " << mNumChannels + // << " larger than max channels = " << MaxChans << "\n"; + // exit(1); + // } + if (mFPP > MaxFPP) { + std::cerr << "*** Regulator.cpp: local FPP = " << mFPP + << " larger than max FPP = " << MaxFPP << "\n"; + exit(1); + } switch (mAudioBitRes) { // int from JitterBuffer to AudioInterface enum case 1: mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT8; @@ -105,13 +141,18 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT32; break; } - mHist = HIST * 32; // samples, from original settings - double histFloat = mHist / (double)mFPP; // packets for other FPP - mHist = (int)histFloat; - if (mHist < 2) - mHist = 2; // min packets for prediction, needs at least 2 - else if (mHist > 6) - mHist = 6; // max packets, keep a lid on CPU load + mHist = HIST; // HIST (default) is 4 + // as FPP decreases the rate of PLC triggers potentially goes up + // and load increases so don't use an inverse relation + + // crossfaded prediction is a full packet ahead of predicted + // packet, so the size of mPrediction needs to account for 2 full packets (2*FPP) + // but trainSamps = (HIST * FPP) and mPrediction.resize(trainSamps - 1, 0.0) so if + // hist = 2, then it exceeds the size + + if (((mNumChannels > 1) && (mFPP > 64)) || (mFPP > 128)) + mHist = 3; // min packets for prediction, needs at least 3 + if (gVerboseFlag) cout << "mHist = " << mHist << " at " << mFPP << "\n"; mBytes = mFPP * mNumChannels * mBitResolutionMode; @@ -124,10 +165,7 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL mFadeDown[i] = 1.0 - mFadeUp[i]; } mLastWasGlitch = false; - mPacketDurMsec = 1000.0 * (double)mFPP / (double)mSampleRate; - if (mMsecTolerance < mPacketDurMsec) - mMsecTolerance = mPacketDurMsec; // absolute minimum - mNumSlots = NumSlotsMax; //((int)ceil(mMsecTolerance / mPacketDurMsec)) + PADSLOTS; + mNumSlots = NumSlotsMax; for (int i = 0; i < mNumSlots; i++) { int8_t* tmp = new int8_t[mBytes]; @@ -143,8 +181,6 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL memcpy(mZeros, mXfrBuffer, mBytes); mAssembledPacket = new int8_t[mBytes]; // for asym memcpy(mAssembledPacket, mXfrBuffer, mBytes); - pushStat = new StdDev(&mIncomingTimer, (int)(floor(48000.0 / (double)mFPP)), 1); - pullStat = new StdDev(&mIncomingTimer, (int)(floor(48000.0 / (double)mFPP)), 2); mLastLostCount = 0; // for stats mIncomingTimer.start(); mLastSeqNumIn = -1; @@ -156,23 +192,27 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL mModSeqNum = mNumSlots * 2; mFPPratioNumerator = 1; mFPPratioDenominator = 1; - mPartialPacketCnt = 0; mFPPratioIsSet = false; mBytesPeerPacket = mBytes; -#ifdef GUIBS3 - // hg for GUI - hg = new HerlperGUI(qApp->activeWindow()); - connect(hg, SIGNAL(moved(double)), this, SLOT(changeGlobal(double))); - connect(hg, SIGNAL(moved_2(int)), this, SLOT(changeGlobal_2(int))); - connect(hg, SIGNAL(moved_3(int)), this, SLOT(changeGlobal_3(int))); -#endif + mAssemblyCnt = 0; + mModCycle = 1; + mModSeqNumPeer = 1; + mPeerFPP = mFPP; // use local until first packet arrives + mAutoHeadroom = DefaultAutoHeadroom; + mFPPdurMsec = 1000.0 * mFPP / 48000.0; changeGlobal_3(LostWindowMax); changeGlobal_2(NumSlotsMax); // need hg if running GUI - changeGlobal((double)qLen); + if (m_b_BroadcastQueueLength) { + m_b_ReceiveRingBuffer = new JitterBuffer( + mFPP, qLen, 48000, 1, m_b_BroadcastQueueLength, mNumChannels, mAudioBitRes); + qDebug() << "Broadcast started in Regulator with packet queue of" + << m_b_BroadcastQueueLength; + // have not implemented the mJackTrip->queueLengthChanged functionality + } } void Regulator::changeGlobal(double x) -{ // mMsecTolerance +{ mMsecTolerance = x; printParams(); } @@ -194,75 +234,112 @@ void Regulator::changeGlobal_3(int x) printParams(); } -void Regulator::printParams() -{ -// qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots -// << "mModSeqNum" << mModSeqNum << "mLostWindow" << mLostWindow; -#ifdef GUIBS3 - updateGUI((int)mMsecTolerance, mNumSlots); -#endif +void Regulator::printParams(){ + // qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots + // << "mModSeqNum" << mModSeqNum << "mLostWindow" << mLostWindow; }; -#ifdef GUIBS3 -void Regulator::updateGUI(double msTol, int nSlots) -{ - hg->updateDisplay(msTol, nSlots, 0); // need to remove last param -} -#endif - Regulator::~Regulator() { - delete mXfrBuffer; - delete mZeros; + delete[] mXfrBuffer; + delete[] mZeros; + delete[] mAssembledPacket; + delete pushStat; + delete pullStat; for (int i = 0; i < mNumChannels; i++) delete mChanData[i]; + for (auto& slot : mSlots) { + delete[] slot; + }; + if (m_b_BroadcastQueueLength) + delete m_b_ReceiveRingBuffer; } -void Regulator::setFPPratio(int len) +void Regulator::setFPPratio() { - int peerFPP = len / (mNumChannels * mBitResolutionMode); - if (peerFPP != mFPP) { - if (peerFPP > mFPP) - mFPPratioDenominator = peerFPP / mFPP; + if (mPeerFPP != mFPP) { + if (mPeerFPP > mFPP) + mFPPratioDenominator = mPeerFPP / mFPP; else - mFPPratioNumerator = mFPP / peerFPP; - qDebug() << "peerBuffers / localBuffers" << mFPPratioNumerator << " / " - << mFPPratioDenominator; + mFPPratioNumerator = mFPP / mPeerFPP; + // qDebug() << "peerBuffers / localBuffers" << mFPPratioNumerator << " / " + // << mFPPratioDenominator; } - if (mFPPratioNumerator > 1) + if (mFPPratioNumerator > 1) { mBytesPeerPacket = mBytes / mFPPratioNumerator; - mFPPratioIsSet = true; + mModCycle = mFPPratioNumerator - 1; + mModSeqNumPeer = mModSeqNum * mFPPratioNumerator; + } else if (mFPPratioDenominator > 1) { + mModSeqNumPeer = mModSeqNum / mFPPratioDenominator; + } } //******************************************************************************* void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) { if (seq_num != -1) { - if (!mFPPratioIsSet) - setFPPratio(len); - if (mFPPratioNumerator > 1) { // 2/1, 4/1 peer FPP is lower - int modSeqNumPeer = mModSeqNum * mFPPratioNumerator; - seq_num %= modSeqNumPeer; - // qDebug() << seq_num << seq_num / mFPPratioNumerator << - // mPartialPacketCnt; - seq_num /= mFPPratioNumerator; - int tmp = (mPartialPacketCnt % mFPPratioNumerator) * mBytesPeerPacket; - memcpy(&mAssembledPacket[tmp], buf, mBytesPeerPacket); - if ((mPartialPacketCnt % mFPPratioNumerator) == (mFPPratioNumerator - 1)) - pushPacket(mAssembledPacket, seq_num); - mPartialPacketCnt++; - } else if (mFPPratioDenominator > 1) { // 1/2, 1/4 peer FPP is higher - int modSeqNumPeer = mModSeqNum / mFPPratioDenominator; - seq_num %= modSeqNumPeer; - seq_num *= mFPPratioDenominator; - for (int i = 0; i < mFPPratioDenominator; i++) { - int tmp = i * mBytes; - memcpy(mAssembledPacket, &buf[tmp], mBytes); - pushPacket(mAssembledPacket, seq_num); - seq_num++; - } - } else + if (!mFPPratioIsSet) { // first peer packet + mPeerFPP = len / (mNumChannels * mBitResolutionMode); + // bufstrategy 1 autoq mode overloads qLen with negative val + // creates this ugly code + if (mMsecTolerance < 0) { // handle -q auto or, for example, -q auto10 + mAuto = true; + // default is -500 from bufstrategy 1 autoq mode + // tweak + if (mMsecTolerance != -500.0) { + // use it to set headroom + mAutoHeadroom = -mMsecTolerance; + qDebug() << "PLC is in auto mode and has been set with" + << mAutoHeadroom << "ms headroom"; + if (mAutoHeadroom > 50.0) + qDebug() << "That's a very large value and should be less than, " + "for example, 50ms"; + } + // found an interesting relationship between mPeerFPP and initial + // mMsecTolerance mPeerFPP*0.5 is pretty good though that's an oddball + // conversion of bufsize directly to msec + mMsecTolerance = (mPeerFPP * AutoInitValFactor); + }; + setFPPratio(); + // number of stats tick calls per sec depends on FPP + int maxFPP = (mPeerFPP > mFPP) ? mPeerFPP : mFPP; + pushStat = + new StdDev(1, &mIncomingTimer, (int)(floor(48000.0 / (double)maxFPP))); + pullStat = + new StdDev(2, &mIncomingTimer, (int)(floor(48000.0 / (double)mFPP))); + mFPPratioIsSet = true; + } + if (mFPPratioNumerator == mFPPratioDenominator) { pushPacket(buf, seq_num); + } else { + seq_num %= mModSeqNumPeer; + if (mFPPratioNumerator > 1) { // 2/1, 4/1 peer FPP is lower, , (local/peer)/1 + int tmp = (seq_num % mFPPratioNumerator) * mBytesPeerPacket; + memcpy(&mAssembledPacket[tmp], buf, mBytesPeerPacket); + if ((seq_num % mFPPratioNumerator) == mModCycle) { + if (mAssemblyCnt == mModCycle) + pushPacket(mAssembledPacket, seq_num / mFPPratioNumerator); + // else + // qDebug() << "incomplete due to lost packet"; + mAssemblyCnt = 0; + } else + mAssemblyCnt++; + } else if (mFPPratioDenominator + > 1) { // 1/2, 1/4 peer FPP is higher, 1/(peer/local) + seq_num *= mFPPratioDenominator; + for (int i = 0; i < mFPPratioDenominator; i++) { + int tmp = i * mBytes; + memcpy(mAssembledPacket, &buf[tmp], mBytes); + pushPacket(mAssembledPacket, seq_num); + seq_num++; + } + } + } + pushStat->tick(); + double adjustAuto = pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec); + // qDebug() << adjustAuto; + if (mAuto && (pushStat->lastTime > AutoInitDur)) + mMsecTolerance = adjustAuto; } }; @@ -270,20 +347,13 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) void Regulator::pushPacket(const int8_t* buf, int seq_num) { QMutexLocker locker(&mMutex); - // qDebug() << "\t" << seq_num; seq_num %= mModSeqNum; - // if (seq_num==0) return; // if (seq_num==1) return; // impose regular loss + // if (seq_num==0) return; // impose regular loss mIncomingTiming[seq_num] = mMsecTolerance + (double)mIncomingTimer.nsecsElapsed() / 1000000.0; mLastSeqNumIn = seq_num; if (mLastSeqNumIn != -1) memcpy(mSlots[mLastSeqNumIn % mNumSlots], buf, mBytes); - double nowMS = pushStat->tick(); - if (mAuto && (nowMS > 2000.0)) { - double tmp = pushStat->longTermStdDev + pushStat->longTermMax; - tmp += 2.0; // 2 ms -- kind of a guess - changeGlobal(tmp); - } }; //******************************************************************************* @@ -291,7 +361,7 @@ void Regulator::pullPacket(int8_t* buf) { QMutexLocker locker(&mMutex); mSkip = 0; - if (mLastSeqNumIn == -1) { + if ((mLastSeqNumIn == -1) || (!mFPPratioIsSet)) { goto ZERO_OUTPUT; } else { mLastSeqNumOut++; @@ -316,16 +386,19 @@ void Regulator::pullPacket(int8_t* buf) } PACKETOK : { - if (mSkip) + if (mSkip) { processPacket(true); - else + pullStat->plcOverruns += mSkip; + } else processPacket(false); + pullStat->tick(); goto OUTPUT; } UNDERRUN : { processPacket(true); pullStat->plcUnderruns++; // count late + pullStat->tick(); goto OUTPUT; } @@ -334,17 +407,28 @@ ZERO_OUTPUT: OUTPUT: memcpy(buf, mXfrBuffer, mBytes); - pullStat->tick(); }; //******************************************************************************* void Regulator::processPacket(bool glitch) { + double tmp = 0.0; + if ((glitch) && (mFPPratioDenominator > 1)) { + glitch = !(mLastSeqNumOut % mFPPratioDenominator); + } + if (glitch) + tmp = (double)mIncomingTimer.nsecsElapsed(); for (int ch = 0; ch < mNumChannels; ch++) processChannel(ch, glitch, mPacketCnt, mLastWasGlitch); mLastWasGlitch = glitch; mPacketCnt++; // 32 bit is good for days: (/ (* (- (expt 2 32) 1) (/ 32 48000.0)) (* 60 60 24)) + + if (glitch) { + double tmp2 = (double)mIncomingTimer.nsecsElapsed() - tmp; + tmp2 /= 1000000.0; + pullStat->lastPLCdspElapsed = tmp2; + } } //******************************************************************************* @@ -367,9 +451,10 @@ void Regulator::processChannel(int ch, bool glitch, int packetCnt, bool lastWasG // LINEAR PREDICT DATA cd->mTail = cd->mTrain; - ba.predict(cd->mCoeffs, cd->mTail); // resizes to TRAINSAMPS-2 + TRAINSAMPS + ba.predict(cd->mCoeffs, + cd->mTail); // resizes to TRAINSAMPS-2 + TRAINSAMPS - for (int i = 0; i < (cd->trainSamps - 1); i++) + for (int i = 0; i < (cd->trainSamps - 2); i++) cd->mPrediction[i] = cd->mTail[i + cd->trainSamps]; } // cross fade last prediction with mTruth @@ -450,7 +535,7 @@ bool BurgAlgorithm::classify(double d) tmp = true; break; case FP_ZERO: - // qDebug() << ("zero"); + qDebug() << ("zero"); tmp = true; break; case FP_SUBNORMAL: @@ -470,8 +555,8 @@ void BurgAlgorithm::train(std::vector& coeffs, const std::vector Ak(m + 1, 0.0); @@ -575,8 +660,9 @@ ChanData::ChanData(int i, int FPP, int hist) : ch(i) } //******************************************************************************* -StdDev::StdDev(QElapsedTimer* timer, int w, int id) : mTimer(timer), window(w), mId(id) +StdDev::StdDev(int id, QElapsedTimer* timer, int w) : mId(id), mTimer(timer), window(w) { + window /= WindowDivisor; reset(); longTermStdDev = 0.0; longTermStdDevAcc = 0.0; @@ -587,21 +673,35 @@ StdDev::StdDev(QElapsedTimer* timer, int w, int id) : mTimer(timer), window(w), longTermMax = 0.0; longTermMaxAcc = 0.0; lastTime = 0.0; + lastPLCdspElapsed = 0.0; data.resize(w, 0.0); } void StdDev::reset() { - mean = 0.0; - // varRunning = 0.0; - acc = 0.0; - min = 999999.0; - max = 0.0; ctr = 0; + plcOverruns = 0; plcUnderruns = 0; + mean = 0.0; + acc = 0.0; + min = 999999.0; + max = -999999.0; +}; + +double StdDev::calcAuto(double autoHeadroom, double localFPPdur) +{ + // qDebug() << longTermStdDev << longTermMax << AutoMax << window << + // longTermCnt; + if ((longTermStdDev == 0.0) || (longTermMax == 0.0)) + return AutoMax; + double tmp = longTermStdDev + ((longTermMax > AutoMax) ? AutoMax : longTermMax); + if (tmp < localFPPdur) + tmp = localFPPdur; // might also check peerFPP... + tmp += autoHeadroom; + return tmp; }; -double StdDev::tick() +void StdDev::tick() { double now = (double)mTimer->nsecsElapsed() / 1000000.0; double msElapsed = now - lastTime; @@ -622,15 +722,15 @@ double StdDev::tick() var += (tmp * tmp); } var /= (double)window; - double stdDev = sqrt(var); + double stdDevTmp = sqrt(var); if (longTermCnt) { - longTermStdDevAcc += stdDev; + longTermStdDevAcc += stdDevTmp; longTermStdDev = longTermStdDevAcc / (double)longTermCnt; longTermMaxAcc += max; longTermMax = longTermMaxAcc / (double)longTermCnt; if (gVerboseFlag) cout << setw(10) << mean << setw(10) << lastMin << setw(10) << max - << setw(10) << stdDev << setw(10) << longTermStdDev << " " << mId + << setw(10) << stdDevTmp << setw(10) << longTermStdDev << " " << mId << endl; } else if (gVerboseFlag) cout << "printing directly from Regulator->stdDev->tick:\n (mean / min / " @@ -638,19 +738,21 @@ double StdDev::tick() "stdDev / longTermStdDev) \n"; longTermCnt++; - lastMean = mean; - lastMin = min; - lastMax = max; - lastStdDev = stdDev; + lastMean = mean; + lastMin = min; + lastMax = max; + lastPlcOverruns = plcOverruns; + lastPlcUnderruns = plcUnderruns; + lastStdDev = stdDevTmp; reset(); } - return lastTime; } + //******************************************************************************* bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset) { QMutexLocker locker(&mMutex); - if (reset) { // all are unused + if (reset) { // all are unused, this is copied from superclass mUnderruns = 0; mOverflows = 0; mSkew0 = mLevel; @@ -661,68 +763,23 @@ bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset) mBufIncCompensate = 0; mBroadcastSkew = 0; } + // hijack of struct IOStat { - stat->underruns = pullStat->lastPlcUnderruns; + stat->underruns = pullStat->lastPlcUnderruns + pullStat->lastPlcOverruns; #define FLOATFACTOR 1000.0 - stat->overflows = FLOATFACTOR * pushStat->longTermStdDev; - stat->skew = FLOATFACTOR * pushStat->lastMean; - stat->skew_raw = FLOATFACTOR * pushStat->lastMin; - stat->level = FLOATFACTOR * pushStat->lastMax; - stat->buf_dec_overflows = FLOATFACTOR * pushStat->lastStdDev; - + stat->overflows = FLOATFACTOR * pushStat->longTermStdDev; + stat->skew = FLOATFACTOR * pushStat->lastMean; + stat->skew_raw = FLOATFACTOR * pushStat->lastMin; + stat->level = FLOATFACTOR * pushStat->lastMax; + // stat->level = FLOATFACTOR * pushStat->longTermMax; + stat->buf_dec_overflows = FLOATFACTOR * pushStat->lastStdDev; + stat->autoq_corr = FLOATFACTOR * mMsecTolerance; stat->buf_dec_pktloss = FLOATFACTOR * pullStat->longTermStdDev; stat->buf_inc_underrun = FLOATFACTOR * pullStat->lastMean; stat->buf_inc_compensate = FLOATFACTOR * pullStat->lastMin; stat->broadcast_skew = FLOATFACTOR * pullStat->lastMax; stat->broadcast_delta = FLOATFACTOR * pullStat->lastStdDev; - // unused - // int32_t autoq_corr; - // int32_t autoq_rate; + stat->autoq_rate = FLOATFACTOR * pullStat->lastPLCdspElapsed; + // none are unused return true; } -/* -QString Regulator::getStats(uint32_t statCount, uint32_t lostCount) -{ - // formatting floats in columns looks better with std::stringstream than with - // QTextStream - QString tmp; - if (!statCount) { - tmp = QString("Regulator: inter-packet intervals msec\n"); - tmp += " (window of last "; - tmp += QString::number(pullStat->window); - tmp += " packets)\n"; - tmp += - "secs avgStdDev (mean min max stdDev) " - "PLC(under over skipped) lost\n"; - } else { - uint32_t lost = lostCount - mLastLostCount; - mLastLostCount = lostCount; -#define PDBL(x) << setw(10) << (QString("%1").arg(pushStat->x, 0, 'f', 2)).toStdString() -#define PDBL2(x) << setw(10) << (QString("%1").arg(pullStat->x, 0, 'f', 2)).toStdString() - std::stringstream logger; - logger << setw(2) - << statCount - PDBL(longTermStdDev) PDBL(lastMean) PDBL(lastMin) PDBL(lastMax) -PDBL(lastStdDev) - << setw(8) << pushStat->lastPlcSkipped - #ifndef GUIBS3 - // comment out this next line for GUI because... - << endl - // ...print all stats in one line when running in GUI because can't -handle extra crlf - // and... to actually see the two lines, need to run it in terminal - #endif - ; - tmp = QString::fromStdString(logger.str()); - std::stringstream logger2; - logger2 << setw(2) - << "" PDBL2(longTermStdDev) PDBL2(lastMean) PDBL2(lastMin) PDBL2(lastMax) - PDBL2(lastStdDev) - << setw(8) << pullStat->lastPlcUnderruns << setw(8) - << pullStat->lastPlcOverruns << setw(8) << pullStat->lastPlcSkipped - << setw(8) << lost << endl; - tmp += QString::fromStdString(logger2.str()); - } - return tmp; -} -*/ diff --git a/src/Regulator.h b/src/Regulator.h index 96d4e0e..9f66afc 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -35,8 +35,7 @@ * \date May 2021 */ -// EXPERIMENTAL for testing in JackTrip v1.4.0 -// Initial references and starter code +// Initial references and starter code to bring up Burg's recursion // http://www.emptyloop.com/technotes/A%20tutorial%20on%20Burg's%20method,%20algorithm%20and%20recursion.pdf // https://metacpan.org/source/SYP/Algorithm-Burg-0.001/README @@ -51,14 +50,6 @@ #include "AudioInterface.h" #include "RingBuffer.h" -//#define GUIBS3 -#ifdef GUIBS3 -#include - -#include "herlpergui.h" -#include "ui_herlpergui.h" -#endif - class BurgAlgorithm { public: @@ -95,46 +86,42 @@ class ChanData class StdDev { public: - StdDev(QElapsedTimer* timer, int w, int id); - void reset(); - double tick(); - QElapsedTimer* mTimer; - std::vector data; - double mean; - double var; - // double varRunning; - int window; + StdDev(int id, QElapsedTimer* timer, int w); + void tick(); + double calcAuto(double autoHeadroom, double localFPPdur); int mId; - double acc; - double min; - double max; - int ctr; + int plcOverruns; + int plcUnderruns; + double lastTime; double lastMean; double lastMin; double lastMax; - int plcUnderruns; + int lastPlcOverruns; int lastPlcUnderruns; + double lastPLCdspElapsed; double lastStdDev; double longTermStdDev; double longTermStdDevAcc; double longTermMax; double longTermMaxAcc; - double lastTime; + + private: + void reset(); + QElapsedTimer* mTimer; + std::vector data; + double mean; + int window; + double acc; + double min; + double max; + int ctr; int longTermCnt; }; -#ifdef GUIBS3 -class Regulator - : public QObject - , public RingBuffer -{ - Q_OBJECT; -#else class Regulator : public RingBuffer { -#endif public: - Regulator(int sample_rate, int channels, int bit_res, int FPP, int qLen); + Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen); virtual ~Regulator(); void shimFPP(const int8_t* buf, int len, int seq_num); @@ -142,31 +129,39 @@ class Regulator : public RingBuffer // can hijack unused2 to propagate incoming seq num if needed // option is in UdpDataProtocol // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num)) - // instread of + // instead of // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size)) - virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, [[maybe_unused]] int len, - [[maybe_unused]] int seq_num) + virtual bool insertSlotNonBlockingRegulator(const int8_t* ptrToSlot, + [[maybe_unused]] int len, + [[maybe_unused]] int seq_num, int lostLen) { shimFPP(ptrToSlot, len, seq_num); + if (m_b_BroadcastQueueLength) + m_b_ReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen); return (true); } void pullPacket(int8_t* buf); virtual void readSlotNonBlocking(int8_t* ptrToReadSlot) { pullPacket(ptrToReadSlot); } + virtual void readBroadcastSlot(int8_t* ptrToReadSlot) + { + m_b_ReceiveRingBuffer->readSlotNonBlocking(ptrToReadSlot); + m_b_ReceiveRingBuffer->readBroadcastSlot(ptrToReadSlot); + } // virtual QString getStats(uint32_t statCount, uint32_t lostCount); virtual bool getStats(IOStat* stat, bool reset); private: - void setFPPratio(int len); + void setFPPratio(); bool mFPPratioIsSet; void processPacket(bool glitch); void processChannel(int ch, bool glitch, int packetCnt, bool lastWasGlitch); int mNumChannels; int mAudioBitRes; int mFPP; - int mSampleRate; + int mPeerFPP; uint32_t mLastLostCount; int mNumSlots; int mHist; @@ -191,7 +186,6 @@ class Regulator : public RingBuffer QElapsedTimer mIncomingTimer; int mLastSeqNumIn; int mLastSeqNumOut; - double mPacketDurMsec; std::vector mPhasor; std::vector mIncomingTiming; int mModSeqNum; @@ -199,16 +193,18 @@ class Regulator : public RingBuffer int mSkip; int mFPPratioNumerator; int mFPPratioDenominator; - int mPartialPacketCnt; + int mAssemblyCnt; + int mModCycle; bool mAuto; -#ifdef GUIBS3 - HerlperGUI* hg; - void updateGUI(double msTol, int nSlots, int lostWin); - public slots: -#endif + int mModSeqNumPeer; + double mAutoHeadroom; + double mFPPdurMsec; void changeGlobal(double); void changeGlobal_2(int); void changeGlobal_3(int); void printParams(); + /// Pointer for the Receive RingBuffer + RingBuffer* m_b_ReceiveRingBuffer; + int m_b_BroadcastQueueLength; }; #endif //__REGULATOR_H__ diff --git a/src/Reverb.cpp b/src/Reverb.cpp index 977fcbd..3f1774d 100644 --- a/src/Reverb.cpp +++ b/src/Reverb.cpp @@ -37,8 +37,6 @@ #include "Reverb.h" -#include - #include "jacktrip_types.h" //******************************************************************************* diff --git a/src/Reverb.h b/src/Reverb.h index 3464660..8970e08 100644 --- a/src/Reverb.h +++ b/src/Reverb.h @@ -41,6 +41,8 @@ #ifndef __REVERB_H__ #define __REVERB_H__ +#include + //#define SINE_TEST #include "ProcessPlugin.h" diff --git a/src/RingBuffer.cpp b/src/RingBuffer.cpp index 9e5ffc7..8e362bb 100644 --- a/src/RingBuffer.cpp +++ b/src/RingBuffer.cpp @@ -69,7 +69,7 @@ RingBuffer::RingBuffer(int SlotSize, int NumSlots) } // Advance write position to half of the RingBuffer - // Udpate Full Slots accordingly + // Update Full Slots accordingly mFullSlots = (NumSlots / 2); mLevelDownRate = 0.01; mStatUnit = 1; @@ -150,7 +150,7 @@ void RingBuffer::readSlotBlocking(int8_t* ptrToReadSlot) bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen) { if (len != mSlotSize && 0 != len) { - // RingBuffer does not suppport mixed buf sizes + // RingBuffer does not support mixed buf sizes return false; } QMutexLocker locker(&mMutex); // lock the mutex @@ -168,7 +168,7 @@ bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int los /// \todo It may be better here to insert the slot anyways, /// instead of not writing anything if (mFullSlots == mNumSlots) { - // std::cout << "OUPUT OVERFLOW NON BLOCKING = " << mNumSlots << std::endl; + // std::cout << "OUTPUT OVERFLOW NON BLOCKING = " << mNumSlots << std::endl; overflowReset(); return true; } @@ -183,6 +183,15 @@ 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 b517511..f7415cb 100644 --- a/src/RingBuffer.h +++ b/src/RingBuffer.h @@ -94,6 +94,12 @@ class 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); + /** \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 1797d32..a40c1e1 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -155,7 +155,7 @@ void RtAudioInterface::setup() try { // IMPORTANT NOTE: It's VERY important to remember to pass this - // as the user data in the process callback, otherwise memeber won't + // as 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, @@ -268,7 +268,7 @@ void RtAudioInterface::printDeviceInfo(unsigned int deviceId) cout << " --Default Output Device--" << endl; } if (info.isDefaultInput) { - cout << " --Default Intput Device--" << endl; + cout << " --Default Input Device--" << endl; } if (info.probed) { cout << " --Probed Successful--" << endl; diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h index 5915ad4..51b6e9d 100644 --- a/src/RtAudioInterface.h +++ b/src/RtAudioInterface.h @@ -60,7 +60,7 @@ class RtAudioInterface : public AudioInterface /// \brief The class destructor virtual ~RtAudioInterface(); - /// \brief List all avialable audio interfaces, with its properties + /// \brief List all available audio interfaces, with its properties virtual void listAllInterfaces(); static void printDevices(); virtual int getDeviceIdFromName(std::string deviceName, bool isInput); diff --git a/src/Settings.cpp b/src/Settings.cpp index 4084c06..e964815 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -1015,7 +1015,7 @@ JackTrip* Settings::getConfiguredJackTrip() jackTrip->setSampleRate(mSampleRate); } - // Change defualt device ID + // Change default device ID if (mChangeDefaultID) { jackTrip->setDeviceID(mDeviceID); } diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index 6f61ad1..43d5df8 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -52,7 +52,7 @@ //#include #include //cc need SD_SEND #else -#include +#include #include // for POSIX Sockets #include #ifndef MANUAL_POLL @@ -217,7 +217,7 @@ int UdpDataProtocol::bindSocket() err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { - // Tell the user that we couldn't find a useable + // Tell the user that we couldn't find a usable // winsock.dll. return INVALID_SOCKET; @@ -226,7 +226,7 @@ int UdpDataProtocol::bindSocket() // Confirm that the Windows Sockets DLL supports 1.1. or higher if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { - // Tell the user that we couldn't find a useable + // Tell the user that we couldn't find a usable // winsock.dll. WSACleanup(); return INVALID_SOCKET; @@ -267,11 +267,36 @@ int UdpDataProtocol::bindSocket() #elif defined(__linux__) ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); #else - // This option is not avialable on Linux, and without it MAC OS X + // This option is not available on Linux, and without it MAC OS X // has problems rebinding a socket ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); #endif +#if defined(_WIN32) + // TODO: these don't seem to work on windows. we likely need to use qWAVE or qos2 +#elif defined(__APPLE__) + // set service type "Interactive Voice" + // TODO: this is supposed to be the right thing to do on OSX, but doesn't seem to do + // anything + const int val = NET_SERVICE_TYPE_VO; + ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val)); +#else + // Set ToS to DSCP Expedited Forwarding (EF), recommended for Audio + // See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474 + // See also + // https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header + const char tos = 0xB8; // 10111000 + if (mIPv6) { + ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + } else { + ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + } + + // Set 802.1q QoS priority + int priority = 6; + ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)); +#endif + // Bind the Socket if (mIPv6) { if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) { @@ -690,7 +715,7 @@ void UdpDataProtocol::waitForReady(int timeout_msec) int loop_resolution_usec = 100; // usecs to wait on each loop int emit_resolution_usec = 10000; // 10 milliseconds int timeout_usec = timeout_msec * 1000; - int elapsed_time_usec = 0; // Ellapsed time in milliseconds + int elapsed_time_usec = 0; // Elapsed time in milliseconds while (!datagramAvailable() && (elapsed_time_usec <= timeout_usec) && !mStopped) { // if (mStopped) { return false; } @@ -811,7 +836,7 @@ void UdpDataProtocol::receivePacketRedundancy( int ok = true; // send audio buf to ok = (mJackTrip->getBufferStrategy() !=3) ? // ring or jitter mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size) - : mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num); + : mJackTrip->writeAudioBufferRegulator(src, host_buf_size, last_seq_num, gap_size); if (!ok) { emit signalError("Local and Peer buffer settings are incompatible"); cout << "ERROR: Local and Peer buffer settings are incompatible" << endl; @@ -879,7 +904,7 @@ void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet, // Move older packets to end of array of redundant packets std::memmove(full_redundant_packet + full_packet_size, full_redundant_packet, full_packet_size * (mUdpRedundancyFactor - 1)); - // Copy new packet to the begining of array + // Copy new packet to the beginning of array std::memcpy(full_redundant_packet, mFullPacket, full_packet_size); // 10% (or other number) packet lost simulation. @@ -929,7 +954,7 @@ void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet, etc... Then, the receiving end checks if the firs packet in the list is the one it should use, - otherwise it continure reding the mUdpRedundancyFactor packets until it finds the one that + otherwise it continue reading the mUdpRedundancyFactor packets until it finds the one that should come next (this can better perfected by just jumping until the correct packet). If it has more than one packet that it hasn't yet received, it sends it to the soundcard one by one. diff --git a/src/UdpDataProtocol.h b/src/UdpDataProtocol.h index d437998..d3ffbe6 100644 --- a/src/UdpDataProtocol.h +++ b/src/UdpDataProtocol.h @@ -61,7 +61,7 @@ * The SENDER and RECEIVER socket can share the same port/address pair (for compatibility * with the JamLink boxes). This is achieved setting * the resusable property in the socket for address and port. You have to - * externaly check if the port is already binded if you want to avoid re-binding to the + * externally check if the port is already binded if you want to avoid re-binding to the * same port. */ class UdpDataProtocol : public DataProtocol @@ -98,9 +98,9 @@ class UdpDataProtocol : public DataProtocol /** \brief Receives a packet. It blocks until a packet is received * - * This function makes sure we recieve a complete packet + * This function makes sure we receive a complete packet * of size n - * \param buf Buffer to store the recieved packet + * \param buf Buffer to store the received packet * \param n size of packet to receive * \return number of bytes read, -1 on error */ @@ -141,7 +141,8 @@ class UdpDataProtocol : public DataProtocol /** \brief Implements the Thread Loop. To start the thread, call start() * ( DO NOT CALL run() ) * - * This function creats and binds all the socket and start the connection loop thread. + * This function creates and binds all the socket and start the connection loop + * thread. */ virtual void run(); @@ -180,14 +181,14 @@ class UdpDataProtocol : public DataProtocol */ void waitForReady(int timeout_msec); - /** \brief Redundancy algorythm at the receiving end + /** \brief Redundancy algorithm at the receiving end */ virtual void receivePacketRedundancy(int8_t* full_redundant_packet, int full_redundant_packet_size, int full_packet_size, uint16_t& current_seq_num, uint16_t& last_seq_num, uint16_t& newer_seq_num); - /** \brief Redundancy algorythm at the sender's end + /** \brief Redundancy algorithm at the sender's end */ virtual void sendPacketRedundancy(int8_t* full_redundant_packet, int full_redundant_packet_size, diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp index 56ab0cc..5ee1806 100644 --- a/src/UdpHubListener.cpp +++ b/src/UdpHubListener.cpp @@ -130,7 +130,7 @@ UdpHubListener::~UdpHubListener() } //******************************************************************************* -// Now that the first handshake is with TCP server, if the addreess/peer port of +// Now that the first handshake is with TCP server, if the address/peer port of // the client is already on the thread pool, it means that a new connection is // requested (the old was disconnected). So we have to remove that thread from // the pool and then connect again. @@ -153,7 +153,7 @@ void UdpHubListener::start() if (mRequireAuth) { cout << "JackTrip HUB SERVER: Enabling authentication" << endl; - // Check that SSL is avaialable + // Check that SSL is available bool error = false; QString error_message; if (!QSslSocket::supportsSsl()) { @@ -316,7 +316,7 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection) // Create a new JackTripWorker, but don't check if this is coming from an existing ip // or port yet. We need to wait until we receive the port value from the UDP header to - // accomodate NAT. + // accommodate NAT. // ----------------------------- int id = getJackTripWorker(PeerAddress.toString(), peer_udp_port, clientName); diff --git a/src/build b/src/build index 5d36142..4e38032 100755 --- a/src/build +++ b/src/build @@ -4,7 +4,7 @@ if [[ -t 1 ]]; then echo -e "\033[1;31mIMPORTANT\033[0m" echo "The build script is now located in the root jacktrip folder and should" echo "be run from there in future. (This script will eventually be removed and" - echo "is included for compatability during the transition.)" + echo "is included for compatibility during the transition.)" echo echo "In future, after cloning the repository with git use the following commands:" echo -e "\033[1m$ cd jacktrip" diff --git a/src/dblsqd/feed.cpp b/src/dblsqd/feed.cpp new file mode 100644 index 0000000..a24215e --- /dev/null +++ b/src/dblsqd/feed.cpp @@ -0,0 +1,332 @@ +#include "feed.h" + +namespace dblsqd +{ + +/*! + \class Feed + * \brief The Feed class provides methods for accessing DBLSQD Feeds and downloading + Releases. + * + * A Feed is a representation of an Application’s Releases. + * This class can retrieve Feeds via HTTP(S) and offers convenience methods for + * + * \section3 Loading Feeds + * + * Before a Feed can be loaded with load(), it needs to be initialized with setUrl(). + * + * \section3 Downloading Updates + * This class also allows downloading updates through the downloadRelease() method. + */ + +/*! + * \brief Constructs a new Feed object. + * + * \sa setUrl() + */ +Feed::Feed(QString baseUrl, QString channel, QString os, QString arch, QString type) + : feedReply(NULL) + , downloadReply(NULL) + , downloadFile(NULL) + , redirects(0) + , _ready(false) +{ + if (!baseUrl.isEmpty()) { + this->setUrl(baseUrl, channel, os, arch, type); + } +} + +/*! + * \brief Sets the Feed URL. + * + * This method can be used to manually set the Feed URL. + */ +void Feed::setUrl(QUrl url) +{ + this->url = url; +} + +/*! + * \brief Sets the Feed URL by specifying its components. + * + * The only required component is baseUrl which must be the base URL for an Application + * provided by the DBSLQD CLI Tool. It should include the full schema and does not require + * a trailing "/". + */ +void Feed::setUrl(QString baseUrl, QString channel, QString os, QString arch, + QString type) +{ + QStringList urlParts; + urlParts << baseUrl; + urlParts << channel; + + if (!os.isEmpty()) { + urlParts << os; + } else { + QString autoOs = QSysInfo::productType().toLower(); + if (autoOs == "windows") { + autoOs = "win"; + } else if (autoOs == "osx" || autoOs == "macos") { + autoOs = "mac"; + } else { + autoOs = QSysInfo::kernelType(); + } + urlParts << autoOs; + } + + if (!arch.isEmpty()) { + urlParts << arch; + } else { + QString autoArch = QSysInfo::buildCpuArchitecture(); + if (autoArch == "i386" || autoArch == "i586" || autoArch == "i586") { + autoArch = "x86"; + } + urlParts << autoArch; + } + + if (!type.isEmpty()) { + urlParts << "?t=" + type; + } + + this->url = QUrl(urlParts.join("/")); +} + +/*! + * \brief Returns the Feed URL. + */ +QUrl Feed::getUrl() +{ + return QUrl(url); +} + +/*! + * \brief Returns a list of all Releases in the Feed. + * + * The list is sorted in descending order by version number/release date. + * If called before ready() was emitted, an empty list is returned. + * \sa getReleases() + */ +QList Feed::getReleases() +{ + return releases; +} + +/*! + * \brief Returns a list of all Releases in the Feed that are newer than the given + * Release. + * + * The list is sorted in descending order by version number/release date. + * If called before ready() was emitted, an empty list is returned. + * \sa getReleases() + */ +QList Feed::getUpdates(Release currentRelease) +{ + QList updates; + for (int i = 0; i < releases.size(); i++) { + if (currentRelease < releases.at(i)) + updates << releases.at(i); + } + return updates; +} + +/*! + * \brief Returns the pointer to a QTemporaryFile for a downloaded file. + * + * If called before downloadFinished() was emitted, this might return a NULL + * pointer. + */ +QTemporaryFile* Feed::getDownloadFile() +{ + return downloadFile; +} + +/*! + * \brief Returns true if Feed information has been retrieved successfully. + * + * A ready Feed might not contain any release information. + * If downloading the Feed failed, false is returned. + */ +bool Feed::isReady() +{ + return _ready; +} + +/* + * Async API functions + */ +/*! + * \brief Retrieves and parses data from the Feed. + * + * A Feed URL must have been set before with setUrl(). Emits ready() or loadError() on + * completion. + */ +void Feed::load() +{ + if (feedReply != NULL && !feedReply->isFinished()) { + return; + } + + QNetworkRequest request(getUrl()); + feedReply = nam.get(request); + connect(feedReply, SIGNAL(finished()), this, SLOT(handleFeedFinished())); +} + +/*! + * \brief Starts the download of a given Release. + * \sa downloadFinished() downloadError() downloadProgress() + */ +void Feed::downloadRelease(Release release) +{ + redirects = 0; + makeDownloadRequest(release.getDownloadUrl()); + this->release = release; +} + +/* + * Private methods + */ +void Feed::makeDownloadRequest(QUrl url) +{ + if (downloadReply != NULL && !downloadReply->isFinished()) { + disconnect(downloadReply); + downloadReply->abort(); + downloadReply->deleteLater(); + } + if (downloadFile != NULL) { + disconnect(downloadFile); + downloadFile->close(); + downloadFile->deleteLater(); + downloadFile = NULL; + } + + QNetworkRequest request(url); + downloadReply = nam.get(request); + connect(downloadReply, SIGNAL(downloadProgress(qint64, qint64)), this, + SLOT(handleDownloadProgress(qint64, qint64))); + connect(downloadReply, SIGNAL(readyRead()), this, SLOT(handleDownloadReadyRead())); + connect(downloadReply, SIGNAL(finished()), this, SLOT(handleDownloadFinished())); +} + +/* + * Signals + */ +/*! \fn void Feed::ready() + * This signal is emitted when a Feed has been successfully downloaded and parsed. + * \sa loadError() load() + */ + +/*! \fn void Feed::loadError(QString message) + * This signal is emitted when a Feed could not be downloaded. + * When loadError() is emitted, ready() is not emitted. + * \sa ready() load() + */ + +/*! \fn void Feed::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) + * This signal is emitted during the download of a Release through downloadRelease(). + * \sa downloadRelease() + */ + +/*! \fn void Feed::downloadFinished() + * This signal is emitted when the download of a Release was successful. + * A QTemporaryFile* of the downloaded file can then be retrieved with getDownloadFile(). + * \sa downloadRelease() + */ + +/*! \fn void Feed::downloadError() + * This signal is emitted when there was an error downloading or verifying a Release. + * When downloadError() is emitted, downloadFinished() is not emitted. + * \sa downloadFinished() downloadRelease() + */ + +/* + * Private Slots + */ +void Feed::handleFeedFinished() +{ + if (feedReply->error() != QNetworkReply::NoError) { + emit loadError(feedReply->errorString()); + return; + } + + releases.clear(); + QByteArray json = feedReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(json); + QJsonArray releasesInfo = doc.object().value("releases").toArray(); + for (int i = 0; i < releasesInfo.size(); i++) { + releases << Release(releasesInfo.at(i).toObject()); + } + std::sort(releases.begin(), releases.end()); + std::reverse(releases.begin(), releases.end()); + + _ready = true; + emit ready(); +} + +void Feed::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + emit downloadProgress(bytesReceived, bytesTotal); +} + +void Feed::handleDownloadReadyRead() +{ + if (downloadFile == NULL) { + QString fileName = downloadReply->url().fileName(); + // Workaround for dblsqd to extract filename via query params from a + // Github-formatted redirect URL + QUrl url = downloadReply->url(); + QString host = url.host(); + if (host.contains("github", Qt::CaseInsensitive) && url.hasQuery()) { + QString query = url.query(); + QRegExp rx("filename%3D(.*)(&|$)"); + rx.setMinimal(true); + if (rx.indexIn(query) > -1) { + fileName = rx.cap(1); + } + } + // End workaround + int extensionPos = fileName.indexOf(QRegExp("(?:\\.tar)?\\.[a-zA-Z0-9]+$")); + if (extensionPos > -1) { + fileName.insert(extensionPos, "-XXXXXX"); + } + downloadFile = new QTemporaryFile(QDir::tempPath() + "/" + fileName); + downloadFile->open(); + } + downloadFile->write(downloadReply->readAll()); +} + +void Feed::handleDownloadFinished() +{ + if (downloadReply->error() != QNetworkReply::NoError) { + emit downloadError(downloadReply->errorString()); + return; + } else if (!downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute) + .isNull()) { + if (redirects >= 8) { + emit downloadError(tr("Too many redirects.")); + return; + } + QUrl redirectionTarget = + downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + QUrl redirectedUrl = downloadReply->url().resolved(redirectionTarget); + redirects++; + makeDownloadRequest(redirectedUrl); + return; + } else if (downloadFile == NULL) { + emit downloadError(tr("No data received from server")); + return; + } + + downloadFile->flush(); + downloadFile->seek(0); + QCryptographicHash fileHash(QCryptographicHash::Sha256); + fileHash.addData(downloadFile->readAll()); + QString hashResult = fileHash.result().toHex(); + if (hashResult.toLower() != release.getDownloadSHA256().toLower()) { + emit downloadError(tr("Could not verify download integrity.")); + return; + } + + emit downloadFinished(); +} + +} // namespace dblsqd diff --git a/src/dblsqd/feed.h b/src/dblsqd/feed.h new file mode 100644 index 0000000..fc030ac --- /dev/null +++ b/src/dblsqd/feed.h @@ -0,0 +1,67 @@ +#ifndef DBLSQD_FEED_H +#define DBLSQD_FEED_H + +#include +#include + +#include "release.h" + +namespace dblsqd +{ + +class Feed : public QObject +{ + Q_OBJECT + + public: + Feed(QString baseUrl = "", QString channel = "release", QString os = QString(), + QString arch = QString(), QString type = QString()); + + void setUrl(QUrl url); + void setUrl(QString baseUrl, QString channel = "release", QString os = QString(), + QString arch = QString(), QString type = QString()); + QUrl getUrl(); + + // Async API + void load(); + void downloadRelease(Release release); + + // Sync API + QList getUpdates( + Release currentRelease = Release(QCoreApplication::applicationVersion())); + QList getReleases(); + QTemporaryFile* getDownloadFile(); + bool isReady(); + + signals: + void ready(); + void loadError(QString message); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadFinished(); + void downloadError(QString message); + + private: + QUrl url; + + QList releases; + + void makeDownloadRequest(QUrl url); + + QNetworkAccessManager nam; + QNetworkReply* feedReply; + Release release; + QNetworkReply* downloadReply; + QTemporaryFile* downloadFile; + uint redirects; + bool _ready; + + private slots: + void handleFeedFinished(); + void handleDownloadProgress(qint64, qint64); + void handleDownloadReadyRead(); + void handleDownloadFinished(); +}; + +} // namespace dblsqd + +#endif // DBLSQD_FEED_H diff --git a/src/dblsqd/release.cpp b/src/dblsqd/release.cpp new file mode 100644 index 0000000..e1c728a --- /dev/null +++ b/src/dblsqd/release.cpp @@ -0,0 +1,143 @@ +#include "release.h" + +namespace dblsqd +{ + +/*! + * \class Release + * \brief This class is used to represent information about a single Release + * from a Feed. + */ + +/*! + * \brief Constructs a new Release from q QJsonObject. + */ +Release::Release(QJsonObject releaseInfo) +{ + this->version = releaseInfo.value("version").toString(); + this->changelog = releaseInfo.value("changelog").toString(); + + QJsonObject downloadInfo = releaseInfo.value("download").toObject(); + this->date = + QDateTime::fromString(downloadInfo.value("date").toString(), Qt::ISODate); + this->downloadUrl = QUrl(downloadInfo.value("url").toString()); + this->downloadSize = downloadInfo.value("size").toDouble(); + this->downloadSHA1 = downloadInfo.value("sha1").toString(); + this->downloadSHA256 = downloadInfo.value("sha256").toString(); + this->downloadDSA = downloadInfo.value("dsa").toString(); +} + +/*! + * \brief Constructs a new Release from a version string and a date. + * + * This method is useful when constructing a "virtual" Release for comparing + * it with Releases retrieved from a Feed. + */ +Release::Release(QString version, QDateTime date) + : version(version) + , date(date) + , changelog("") + , downloadUrl("") + , downloadSize(0) + , downloadSHA1("") + , downloadSHA256("") + , downloadDSA("") +{ +} + +/*! + * \brief Compares two Releases. + * + * If the Release version is compatible with SemVer, the version determines + * Release order. If versions are identical or not compatible with SemVer, + * Release date is used for determining order instead. + */ +bool operator<(const Release& one, const Release& other) +{ + SemVer v1(one.version); + SemVer v2(other.version); + if (v1.isValid() && v2.isValid()) { + return (v1 < v2); + } else { + return (one.date < other.date); + } +} + +bool operator==(const Release& one, const Release& other) +{ + return one.version == other.version; +} + +bool operator<=(const Release& one, const Release& other) +{ + return one == other || one < other; +} + +/* + * Getters + */ +/*! + * \brief Returns the Release version. + */ +QString Release::getVersion() const +{ + return this->version; +} + +/*! + * \brief Returns the Release changelog. + */ +QString Release::getChangelog() const +{ + return this->changelog; +} + +/*! + * \brief Returns the Release date. + */ +QDateTime Release::getDate() const +{ + return this->date; +} + +/*! + * \brief Returns the Release download URL. + */ +QUrl Release::getDownloadUrl() const +{ + return this->downloadUrl; +} + +/*! + * \brief Returns the SHA1 hash of the Release download. + */ +QString Release::getDownloadSHA1() const +{ + return this->downloadSHA1; +} + +/*! + * \brief Returns the SHA256 hash of the Release download. + */ +QString Release::getDownloadSHA256() const +{ + return this->downloadSHA256; +} + +/*! + * \brief Returns the DSA signature of the Release download. + */ +QString Release::getDownloadDSA() const +{ + return this->downloadDSA; +} + +/*! + * \brief Returns the size of the Release download in bytes. + */ +qint64 Release::getDownloadSize() const +{ + return (qint64)this->downloadSize; +} + +} // namespace dblsqd diff --git a/src/dblsqd/release.h b/src/dblsqd/release.h new file mode 100644 index 0000000..9c769bf --- /dev/null +++ b/src/dblsqd/release.h @@ -0,0 +1,44 @@ +#ifndef DBLSQD_RELEASE_H +#define DBLSQD_RELEASE_H + +#include +#include + +#include "semver.h" + +namespace dblsqd +{ + +class Release +{ + public: + Release(QJsonObject releaseInfo); + Release(QString version = QString(), QDateTime date = QDateTime()); + + friend bool operator<(const Release& one, const Release& other); + friend bool operator==(const Release& one, const Release& other); + friend bool operator<=(const Release& one, const Release& other); + + QString getVersion() const; + QString getChangelog() const; + QDateTime getDate() const; + QUrl getDownloadUrl() const; + QString getDownloadSHA1() const; + QString getDownloadSHA256() const; + QString getDownloadDSA() const; + qint64 getDownloadSize() const; + + private: + QString version; + QDateTime date; + QString changelog; + QUrl downloadUrl; + long downloadSize; + QString downloadSHA1; + QString downloadSHA256; + QString downloadDSA; +}; + +} // namespace dblsqd + +#endif // DBLSQD_RELEASE_H diff --git a/src/dblsqd/semver.cpp b/src/dblsqd/semver.cpp new file mode 100644 index 0000000..0aa77d0 --- /dev/null +++ b/src/dblsqd/semver.cpp @@ -0,0 +1,87 @@ +#include "semver.h" + +namespace dblsqd +{ + +/*! + * \class SemVer + * \brief SemVer encapsulates a version according to + * Semantic Versioning 2.0. + */ + +/*! + * \brief Constructs a new SemVer object from a string. + */ +SemVer::SemVer(QString version) : original(version), valid(false) +{ + QRegExp rx(getRegExp()); + if (rx.indexIn(version) > -1) { + this->major = rx.cap(1).toInt(); + this->minor = rx.cap(2).toInt(); + this->patch = rx.cap(3).toInt(); + this->prerelease = rx.cap(4); + this->build = rx.cap(5); + this->valid = true; + } else { + this->major = 0; + this->minor = 0; + this->patch = 0; + this->prerelease = ""; + this->build = ""; + this->valid = false; + } +} + +/*! + * \brief Returns true if this version is valid according to the SemVer + * specification. Otherwise returns false. + */ +bool SemVer::isValid() const +{ + return this->valid; +} + +/*! + * \brief Compares two SemVer objects. + * + * Returns true if the left-hand SemVer object represents a lower version + * according to the SemVer 2.0 specification. + * Otherweise returns false. + * Returns false if one of the SemVer objects does not represent a valid + * SemVer. + * \sa isValid() + */ +bool SemVer::operator<(const SemVer& other) +{ + if (!this->isValid() || !other.isValid()) { + return false; + } + + if (this->major != other.major) { + return this->major < other.major; + } else if (this->minor != other.minor) { + return this->minor < other.minor; + } else if (this->patch != other.patch) { + return this->patch < other.patch; + } else if (this->prerelease != other.prerelease) { + if (this->prerelease == "") { + return false; + } else if (other.prerelease == "") { + return true; + } + return (QString::localeAwareCompare(this->prerelease, other.prerelease) < 0); + } else { + return (QString::localeAwareCompare(this->build, other.build) < 0); + } +} + +QString SemVer::getRegExp() +{ + QString v = "(0|[1-9]\\d*)"; + QString p = + "(?:-((?:0|[1-9A-Za-z][0-9A-Za-z]*)(?:\\.(?:0|[1-9A-Za-z][0-9A-Za-z]*))*))?"; + QString b = "(?:\\+((?:[0-9A-Za-z]*)(?:\\.(?:[0-9A-Za-z][0-9A-Za-z]*))*))?"; + return "^" + v + "." + v + "." + v + p + b + "$"; +} + +} // namespace dblsqd diff --git a/src/dblsqd/semver.h b/src/dblsqd/semver.h new file mode 100644 index 0000000..18ebcda --- /dev/null +++ b/src/dblsqd/semver.h @@ -0,0 +1,34 @@ +#ifndef DBLSQD_SEMVER_H +#define DBLSQD_SEMVER_H + +#include +#include + +namespace dblsqd +{ + +class SemVer +{ + public: + SemVer(QString version); + + bool operator<(const SemVer& other); + + bool isValid() const; + QString toString(); + + private: + QString original; + int major; + int minor; + int patch; + QString prerelease; + QString build; + bool valid; + + static QString getRegExp(); +}; + +} // namespace dblsqd + +#endif // DBLSQD_SEMVER_H diff --git a/src/dblsqd/update_dialog.cpp b/src/dblsqd/update_dialog.cpp new file mode 100644 index 0000000..bb914ef --- /dev/null +++ b/src/dblsqd/update_dialog.cpp @@ -0,0 +1,694 @@ +#include "update_dialog.h" + +#include "ui_update_dialog.h" + +namespace dblsqd +{ + +/*! + * \class UpdateDialog + * \brief A dialog class for displaying and downloading update information. + * + * UpdateDialog is a drop-in class for adding a fully-functional auto-update + * component to an existing application. + * + * The most simple integration is + * possible with just three lines of code: + * \code + * dblsqd::Feed* feed = new dblsqd::Feed(); + * feed->setUrl("https://feeds.dblsqd.com/:app_token"); + * dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed); + * \endcode + * + * The update dialog can also display an application icon which can be set with + * setIcon(). + */ + +/*! + * \enum UpdateDialog::Type + * \brief This flag determines the if and when the UpdateDialog is displayed + * automatically. + * + * *OnUpdateAvailable*: Automatically display the dialog as soon as the Feed + * has been downloaded and parsed and if there is a newer version than the + * current version returned by QCoreApplication::applicationVersion(). + * + * *OnLastWindowClosed*: If there is a newer version available than the current + * version returned by QCoreApplication::applicationVersion(), the update + * dialog is displayed when QGuiApplication emits the lastWindowClosed() event. + * Note that when this flag is used, + * QGuiApplication::setQuitOnLastWindowClosed(false) will be called. + * + * *Manual*: The dialog is only displayed when explicitly requested via show() + * or exec(). + * Note that update information might not be available instantly after + * constructing an UpdateDialog. + * + * *ManualChangelog*: The dialog is only displayed when explicitly requested via + * show() or exec(). + * Instead of the full update interface, only the changelog will be shown. + */ + +/*! + * \brief Constructs a new UpdateDialog. + * + * A Feed object needs to be constructed first and passed to this constructor. + * Feed::load() does not need to be called on the Feed object. + * + * The given UpdateDialog::Type flag determines when/if the dialog is shown + * automatically. + * + * UpdateDialog uses QSettings to save information such as when a release was + * skipped by the users. If you want to use a specially initialized QSettings + * object, you may also pass it to this constructor. + * + */ +UpdateDialog::UpdateDialog(Feed* feed, int type, QWidget* parent, QSettings* settings) + : QDialog(parent) + , ui(new Ui::UpdateDialog) + , feed(feed) + , type(type) + , settings(settings) + , accepted(false) + , isDownloadFinished(false) + , acceptedInstallButton(NULL) +{ + ui->setupUi(this); + + QPalette palette = this->palette(); + QString textColor = palette.color(QPalette::Text).name(); + QString backgroundColor = palette.color(QPalette::Base).name(); + QString labelChangelogStyle = + QString("color: %1; background: %2").arg(textColor, backgroundColor); + ui->labelChangelog->setStyleSheet(labelChangelogStyle); + + ui->buttonCancel->addAction(ui->actionCancel); + ui->buttonCancel->addAction(ui->actionSkip); + ui->buttonCancel->setDefaultAction(ui->actionCancel); + + _openExternalLinks = true; + connect(ui->labelChangelog, SIGNAL(linkActivated(QString)), this, + SLOT(onLinkActivated(QString))); + + switch (type) { + case OnUpdateAvailable: { + connect(this, SIGNAL(ready()), this, SLOT(showIfUpdatesAvailable())); + break; + } + case OnLastWindowClosed: { + QGuiApplication* app = (QGuiApplication*)QApplication::instance(); + app->setQuitOnLastWindowClosed(false); + connect(app, SIGNAL(lastWindowClosed()), this, + SLOT(showIfUpdatesAvailableOrQuit())); + break; + } + case Manual: { + // don’t do anything + } + } + + if (feed->isReady()) { + handleFeedReady(); + } else { + setupLoadingUi(); + feed->load(); + connect(feed, SIGNAL(ready()), this, SLOT(handleFeedReady())); + } +} + +UpdateDialog::~UpdateDialog() +{ + delete ui; +} + +/* + * Setters + */ +/*! + * \brief Sets the icon displayed in the update window. + */ +void UpdateDialog::setIcon(QPixmap pixmap) +{ + ui->labelIcon->setPixmap(QPixmap(pixmap)); + ui->labelIcon->setHidden(false); +} + +void UpdateDialog::setIcon(QString fileName) +{ + ui->labelIcon->setPixmap(QPixmap(fileName)); + ui->labelIcon->setHidden(false); +} + +/*! + * \brief Sets the minimum version to be displayed in the changelog. + * Defaults to QApplication::applicationVersion() if not set. + * \param version + */ +void UpdateDialog::setMinVersion(QString version) +{ + _minVersion = version; + setupChangelogUi(); +} + +/*! + * \brief Sets the maximum version to be displayed in the changelog + * \param version + */ +void UpdateDialog::setMaxVersion(QString version) +{ + _maxVersion = version; + setupChangelogUi(); +} + +/*! + * \brief Convenience method for setting minimum and maximum version to be displayed in + * the changelog. maximumVersion is set to QApplication::applicationVersion() + * + * \param previousVersion + */ +void UpdateDialog::setPreviousVersion(QString previousVersion) +{ + _previousVersion = previousVersion; + _minVersion = previousVersion; + _maxVersion = QApplication::applicationVersion(); + setupChangelogUi(); +} + +/*! + * \brief Adds a custom button for handling update installation. + * \param button + * + * When the custom button is clicked after an update has been downloaded or when + * downloading an update that was started by clicking the button has finished, + * installButtonClicked(QAbstractButton* button, QString filePath) is emitted. + */ +void UpdateDialog::addInstallButton(QAbstractButton* button) +{ + installButtons.append(button); + ui->buttonContainer->layout()->addWidget(button); + if (isVisible() && ui->buttonCancel->isVisible()) { + setupUpdateUi(); + } +} + +/*! + * \propget UpdateExternalLinks + * + * Determines if links in the changelog should be opened automatically by + * QDesktopServices::openUrl() when a user clicks on them. + * If set to false, the linkActivated() signal is emitted instead. + * + * The default value is true. + + */ +bool UpdateDialog::openExternalLinks() +{ + return _openExternalLinks; +} + +/*! + * \propset UpdateDialog::openExternalLinks + */ +void UpdateDialog::setOpenExternalLinks(bool open) +{ + _openExternalLinks = open; +} + +/* + * Public Slots + */ +/*! + * \brief Default handler for the install button. + * + * Closes the dialog if no other action (such as + * downloading or installing a Release) is required first. + */ +void UpdateDialog::onButtonInstall() +{ + accepted = true; + if (isDownloadFinished) { + startUpdate(); + } else if (!latestRelease.getVersion().isEmpty()) { + startDownload(); + } else { + done(QDialog::Accepted); + } +} + +void UpdateDialog::onButtonCustomInstall() +{ + accepted = true; + if (isDownloadFinished) { + emit installButtonClicked((QAbstractButton*)sender(), updateFilePath); + } else if (!latestRelease.getVersion().isEmpty()) { + acceptedInstallButton = (QAbstractButton*)sender(); + startDownload(); + } else { + done(QDialog::Accepted); + } +} + +/*! + * \brief Skips the latest retrieved Release. + * + * If a release has been skipped, UpdateDialog will not be displayed + * automatically when using Type::OnUpdateAvailable or + * Type::OnLastWindowClosed. + */ +void UpdateDialog::skip() +{ + if (!updateFilePath.isEmpty()) { + QFile::remove(updateFilePath); + } + setSettingsValue("skipRelease", latestRelease.getVersion(), settings); + done(QDialog::Rejected); +} + +/*! + * \brief Shows the dialog if there are available updates. + */ +void UpdateDialog::showIfUpdatesAvailable() +{ + QString latestVersion = latestRelease.getVersion(); + bool skipRelease = + (settingsValue("skipRelease", "", settings).toString() == latestVersion); + if (!latestVersion.isEmpty() && !skipRelease) { + show(); + } +} + +/*! + * \brief Shows the dialog if there are updates available or quits the application. + */ +void UpdateDialog::showIfUpdatesAvailableOrQuit() +{ + if (type == OnLastWindowClosed) { + QGuiApplication* app = (QGuiApplication*)QApplication::instance(); + app->setQuitOnLastWindowClosed(true); + disconnect(app, SIGNAL(lastWindowClosed()), this, + SLOT(showIfUpdatesAvailableOrQuit())); + } + QString latestVersion = latestRelease.getVersion(); + bool skipRelease = + (settingsValue("skipRelease", "", settings).toString() == latestVersion); + if (!latestVersion.isEmpty() && !skipRelease) { + show(); + } else { + QCoreApplication::quit(); + } +} + +/* + * Static settings helpers + */ +QVariant UpdateDialog::settingsValue(QString key, QVariant defaultValue, + QSettings* settings) +{ + return settings->value("DBLSQD/" + key, defaultValue); +} + +void UpdateDialog::setSettingsValue(QString key, QVariant value, QSettings* settings) +{ + settings->setValue("DBLSQD/" + key, value); +} + +void UpdateDialog::removeSetting(QString key, QSettings* settings) +{ + settings->remove("DBLSQD/" + key); +} + +void UpdateDialog::setDefaultSettingsValue(QString key, QVariant value, + QSettings* settings) +{ + if (settings->contains("DBLSQD/" + key)) + return; + setSettingsValue(key, value, settings); +} + +/*! + * \brief Enables or disables automatic downloads. + */ +void UpdateDialog::enableAutoDownload(bool enabled, QSettings* settings) +{ + setSettingsValue("autoDownload", enabled, settings); +} + +/*! + * \brief Returns true if automatic downloads are enabled. + * + * If defaultValue is provided, it is stored if no other value has previously been set. + */ +bool UpdateDialog::autoDownloadEnabled(QVariant defaultValue, QSettings* settings) +{ + if (defaultValue.isValid()) { + setDefaultSettingsValue("autoDownload", defaultValue, settings); + } else { + defaultValue = false; + } + return settingsValue("autoDownload", defaultValue, settings).toBool(); +} + +/*! + * \overload + */ +bool UpdateDialog::autoDownloadEnabled(QSettings* settings) +{ + return settingsValue("autoDownload", false, settings).toBool(); +} + +/* + * Helpers + */ + +void UpdateDialog::adjustDialogSize() +{ + adjustSize(); + +/*HACK: Qt seems to incorrectly calculate window geometry on Windows. + This code avoids warning messages logged by the application + in that case.*/ +#if defined(Q_OS_WIN) || defined(Q_WS_WIN) + QSize dialogSize = size(); + resize(dialogSize.width(), dialogSize.height() + 3); +#endif +} + +void UpdateDialog::resetUi() +{ + QList hiddenWidgets; + for (int i = 0; i < installButtons.size(); i++) { + hiddenWidgets << installButtons.at(i); + } + hiddenWidgets << ui->headerContainer << ui->labelIcon << ui->headerContainerLoading + << ui->headerContainerNoUpdates << ui->headerContainerChangelog + << ui->scrollAreaChangelog << ui->progressBar << ui->checkAutoDownload + << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm + << ui->buttonInstall; + for (int i = 0; i < hiddenWidgets.size(); i++) { + hiddenWidgets.at(i)->hide(); + hiddenWidgets.at(i)->disconnect(); + } + ui->progressBar->reset(); + adjustDialogSize(); +} + +void UpdateDialog::setupLoadingUi() +{ + resetUi(); + ui->headerContainerLoading->show(); + ui->progressBar->show(); + ui->progressBar->setMaximum(0); + ui->progressBar->setMinimum(0); + ui->buttonCancelLoading->show(); + ui->buttonCancelLoading->setFocus(); + connect(ui->buttonCancelLoading, SIGNAL(clicked(bool)), this, SLOT(reject())); + adjustDialogSize(); +} + +void UpdateDialog::setupUpdateUi() +{ + resetUi(); + + QList showWidgets; + showWidgets << ui->headerContainer << ui->scrollAreaChangelog << ui->checkAutoDownload + << ui->buttonCancel << ui->buttonInstall; + for (int i = 0; i < showWidgets.size(); i++) { + showWidgets.at(i)->show(); + } + + QList labels; + labels << ui->labelHeadline << ui->labelInfo; + for (int i = 0; i < labels.size(); i++) { + QString text = labels.at(i)->text(); + replaceAppVars(text); + labels.at(i)->setText(text); + } + ui->labelChangelog->setText(generateChangelogDocument()); + + ui->checkAutoDownload->setChecked(autoDownloadEnabled(settings)); + + // Adapt buttons if release has been downloaded already + if (isDownloadFinished) { + ui->progressBar->show(); + ui->progressBar->setMaximum(1); + ui->progressBar->setValue(1); + } + + connect(feed, SIGNAL(downloadFinished()), this, SLOT(handleDownloadFinished())); + connect(feed, SIGNAL(downloadError(QString)), this, + SLOT(handleDownloadError(QString))); + connect(feed, SIGNAL(downloadProgress(qint64, qint64)), this, + SLOT(updateProgressBar(qint64, qint64))); + + connect(ui->buttonConfirm, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->actionCancel, SIGNAL(triggered()), this, SLOT(reject())); + connect(ui->actionSkip, SIGNAL(triggered()), this, SLOT(skip())); + connect(ui->checkAutoDownload, SIGNAL(toggled(bool)), this, + SLOT(autoDownloadCheckboxToggled(bool))); + + // Install buttons + if (installButtons.isEmpty()) { + ui->buttonInstall->setFocus(); + connect(ui->buttonInstall, SIGNAL(clicked()), this, SLOT(onButtonInstall())); + } else { + ui->buttonInstall->hide(); + for (int i = 0; i < installButtons.size(); i++) { + installButtons.at(i)->show(); + connect(installButtons.at(i), SIGNAL(clicked(bool)), this, + SLOT(onButtonCustomInstall())); + } + installButtons.last()->setFocus(); + } + + adjustDialogSize(); +} + +void UpdateDialog::setupChangelogUi() +{ + resetUi(); + + QList showWidgets; + showWidgets << ui->headerContainerChangelog << ui->buttonConfirm + << ui->scrollAreaChangelog; + for (int i = 0; i < showWidgets.size(); i++) { + showWidgets.at(i)->show(); + } + QList labels; + labels << ui->labelHeadlineChangelog << ui->labelInfoChangelog; + for (int i = 0; i < labels.size(); i++) { + QString text = labels.at(i)->text(); + replaceAppVars(text); + labels.at(i)->setText(text); + } + ui->labelChangelog->setText(generateChangelogDocument()); + connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept())); + ui->buttonConfirm->setFocus(); + adjustDialogSize(); +} + +void UpdateDialog::setupNoUpdatesUi() +{ + resetUi(); + QList showWidgets; + showWidgets << ui->headerContainerNoUpdates << ui->buttonConfirm; + for (int i = 0; i < showWidgets.size(); i++) { + showWidgets.at(i)->show(); + } + ui->buttonConfirm->setFocus(); + + QString text = ui->labelHeadlineNoUpdates->text(); + replaceAppVars(text); + ui->labelHeadlineNoUpdates->setText(text); + + connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept())); + adjustDialogSize(); +} + +void UpdateDialog::disableButtons(bool disable) +{ + QList buttons; + for (int i = 0; i < installButtons.size(); i++) { + buttons << installButtons.at(i); + } + buttons << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm + << ui->buttonConfirm << ui->buttonInstall << ui->checkAutoDownload; + for (int i = 0; i < buttons.size(); i++) { + buttons.at(i)->setDisabled(disable); + } +} + +void UpdateDialog::replaceAppVars(QString& string) +{ + string.replace("%APPNAME%", QCoreApplication::applicationName()); + string.replace("%CURRENT_VERSION%", QCoreApplication::applicationVersion()); + string.replace("%UPDATE_VERSION%", latestRelease.getVersion()); +} + +QString UpdateDialog::generateChangelogDocument() +{ + QString changelog; + QList changelogReleases; + if (_minVersion.isEmpty() && _maxVersion.isEmpty()) { + changelogReleases = updates; + } else { + Release minRelease(_minVersion.isEmpty() ? QApplication::applicationVersion() + : _minVersion); + Release maxRelease(_maxVersion); + for (int i = 0; i < releases.size(); i++) { + if (minRelease < releases.at(i) + && (_maxVersion.isEmpty() || releases.at(i) <= maxRelease)) { + changelogReleases << releases.at(i); + } + } + } + for (int i = 0; i < changelogReleases.size(); i++) { + QString h2Style = "font-size: medium;"; + if (i > 0) { + h2Style.append("margin-top: 1em;"); + } + changelog.append("

" + + changelogReleases.at(i).getVersion() + "

"); + changelog.append("

" + changelogReleases.at(i).getChangelog() + "

"); + } + return changelog; +} + +void UpdateDialog::startDownload() +{ + feed->downloadRelease(latestRelease); + disableButtons(true); +} + +void UpdateDialog::startUpdate() +{ + if (QDesktopServices::openUrl(QUrl::fromLocalFile(updateFilePath))) { + done(QDialog::Accepted); + QApplication::quit(); + } else { + handleDownloadError(tr("Could not open downloaded file %1").arg(updateFilePath)); + } +} + +/* + * Private Slots + */ + +void UpdateDialog::autoDownloadCheckboxToggled(bool enabled) +{ + enableAutoDownload(enabled, settings); +} + +void UpdateDialog::handleFeedReady() +{ + // Retrieve update information + Release currentRelease(QApplication::applicationVersion()); + updates = feed->getUpdates(currentRelease); + releases = feed->getReleases(); + if (!updates.isEmpty()) { + latestRelease = updates.first(); + } + + if (type == ManualChangelog) { + setupChangelogUi(); + emit ready(); + return; + } + + // Check if an update has been downloaded previously + updateFilePath = settingsValue("updateFilePath", "", settings).toString(); + if (!updateFilePath.isEmpty() && QFile::exists(updateFilePath)) { + QString updateFileVersion = + settingsValue("updateFileVersion", "", settings).toString(); + if (updateFileVersion != latestRelease.getVersion() + || updateFileVersion == QApplication::applicationVersion()) { + QFile::remove(updateFilePath); + removeSetting("updateFilePath"); + removeSetting("updateFileVersion"); + updateFilePath = ""; + } else { + isDownloadFinished = true; + } + } + + // Check if there are any updates + if (updates.isEmpty()) { + setupNoUpdatesUi(); + return; + } + + // Automatic downloads + QString latestVersion = latestRelease.getVersion(); + bool skipRelease = + (settingsValue("skipRelease", "", settings).toString() == latestVersion); + bool autoDownload = autoDownloadEnabled(settings) && (!skipRelease); + if (autoDownload && !isDownloadFinished) { + startDownload(); + } + + // Setup UI + setupUpdateUi(); + emit ready(); +} + +void UpdateDialog::handleDownloadFinished() +{ + QTemporaryFile* file = feed->getDownloadFile(); + isDownloadFinished = true; + updateFilePath = file->fileName(); + file->setAutoRemove(false); + file->close(); + file->deleteLater(); + setSettingsValue("updateFilePath", updateFilePath, settings); + setSettingsValue("updateFileVersion", latestRelease.getVersion(), settings); + + if (accepted) { + if (acceptedInstallButton == NULL) { + startUpdate(); + } else { + emit installButtonClicked(acceptedInstallButton, updateFilePath); + } + + } else { + disableButtons(false); + } +} + +void UpdateDialog::handleDownloadError(QString message) +{ + QMessageBox* messageBox = new QMessageBox(this); + messageBox->setIcon(QMessageBox::Warning); + messageBox->setText("There was an error while downloading the update."); + messageBox->setInformativeText(message); + messageBox->show(); + done(QDialog::Rejected); +} + +void UpdateDialog::updateProgressBar(qint64 bytesReceived, qint64 bytesTotal) +{ + ui->progressBar->show(); + ui->progressBar->setMaximum(bytesTotal / 1024); + ui->progressBar->setValue(bytesReceived / 1024); +} + +void UpdateDialog::onLinkActivated(QString link) +{ + if (_openExternalLinks) { + QDesktopServices::openUrl(link); + } else { + emit linkActivated(link); + } +} + +/* + * Signals + */ +/*! \fn void Feed::ready() + * This signal is emitted when a updates are available and the UpdateDialog is + * ready to be shown with show() or exec(). + */ + +/*! \fn void Feed::installButtonClicked(QAbstractButton* button, QString filePath) + * This signal is emitted when a custom install button was clicked. + */ + +} // namespace dblsqd diff --git a/src/dblsqd/update_dialog.h b/src/dblsqd/update_dialog.h new file mode 100644 index 0000000..ce49ea8 --- /dev/null +++ b/src/dblsqd/update_dialog.h @@ -0,0 +1,106 @@ +#ifndef DBLSQD_UPDATE_DIALOG_H +#define DBLSQD_UPDATE_DIALOG_H + +#include +#include +#include +#include +#include + +#include "feed.h" +#include "ui_update_dialog.h" + +namespace dblsqd +{ + +class UpdateDialog : public QDialog +{ + Q_OBJECT + + public: + enum Type { OnUpdateAvailable, OnLastWindowClosed, Manual, ManualChangelog }; + explicit UpdateDialog(Feed* feed, int = OnUpdateAvailable, QWidget* parent = 0, + QSettings* settings = new QSettings()); + ~UpdateDialog(); + + void setIcon(QString fileName); + void setIcon(QPixmap pixmap); + void addInstallButton(QAbstractButton* button); + + void setMinVersion(QString version); + void setMaxVersion(QString version); + void setPreviousVersion(QString version); + + static bool autoDownloadEnabled(QVariant defaultValue, + QSettings* settings = new QSettings); + static bool autoDownloadEnabled(QSettings* settings = new QSettings()); + static void enableAutoDownload(bool enabled, QSettings* settings = new QSettings); + + void setOpenExternalLinks(bool open); + bool openExternalLinks(); + + signals: + void ready(); + void installButtonClicked(QAbstractButton* button, QString filePath); + void linkActivated(QString link); + + public slots: + void onButtonInstall(); + void onButtonCustomInstall(); + void skip(); + void showIfUpdatesAvailable(); + void showIfUpdatesAvailableOrQuit(); + + private: + Ui::UpdateDialog* ui; + Feed* feed; + int type; + + QSettings* settings; + void replaceAppVars(QString& string); + QString generateChangelogDocument(); + + void disableButtons(bool disable = true); + void resetUi(); + void setupLoadingUi(); + void setupUpdateUi(); + void setupChangelogUi(); + void setupNoUpdatesUi(); + void adjustDialogSize(); + + void startDownload(); + virtual void startUpdate(); + + bool accepted; + bool isDownloadFinished; + QString updateFilePath; + QList releases; + QList updates; + Release latestRelease; + QList installButtons; + QAbstractButton* acceptedInstallButton; + bool _openExternalLinks; + QString _minVersion; + QString _maxVersion; + QString _previousVersion; + + static void setSettingsValue(QString key, QVariant value, + QSettings* settings = new QSettings()); + static QVariant settingsValue(QString key, QVariant defaultValue = QVariant(), + QSettings* settings = new QSettings()); + static void removeSetting(QString key, QSettings* settings = new QSettings()); + static void setDefaultSettingsValue(QString key, QVariant value, + QSettings* settings = new QSettings()); + + private slots: + void handleFeedReady(); + void handleDownloadFinished(); + void handleDownloadError(QString); + void updateProgressBar(qint64, qint64); + void autoDownloadCheckboxToggled(bool enabled = true); + void onLinkActivated(QString link); +}; + +} // namespace dblsqd + +#endif // DBLSQD_UPDATE_DIALOG_H diff --git a/src/dblsqd/update_dialog.ui b/src/dblsqd/update_dialog.ui new file mode 100644 index 0000000..8bb40e4 --- /dev/null +++ b/src/dblsqd/update_dialog.ui @@ -0,0 +1,402 @@ + + + UpdateDialog + + + + 0 + 0 + 600 + 645 + + + + + 600 + 0 + + + + true + + + + + + + 0 + + + 6 + + + 0 + + + 0 + + + + + + 75 + true + + + + Loading update information … + + + + + + + + + + + 0 + + + 6 + + + 0 + + + 18 + + + 24 + + + 6 + + + + + + 75 + true + + + + A new version of %APPNAME% is available! + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + %APPNAME% %UPDATE_VERSION% is available (you have %CURRENT_VERSION%). +Would you like to update now? + + + true + + + + + + + + + + + 0 + + + 6 + + + 0 + + + 18 + + + + + + 75 + true + + + + Changelog for %APPNAME% + + + + + + + You are using version %CURRENT_VERSION%. + + + + + + + + + + + 0 + + + 6 + + + 0 + + + 18 + + + 24 + + + + + There are currently no updates available. + + + true + + + + + + + + 75 + true + + + + You are using %APPNAME% %CURRENT_VERSION%. + + + true + + + + + + + + + + + 0 + 0 + + + + + 0 + 150 + + + + true + + + + + 0 + 0 + 580 + 235 + + + + + 0 + 0 + + + + background:white; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + 5 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + 24 + + + + + + + Automatically download future updates + + + + + + + + 0 + 0 + + + + + 12 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::ActionsContextMenu + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonFollowStyle + + + true + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Install update now + + + + + + + OK + + + + + + + + + + Remind me later + + + + + Skip this version + + + + + + checkAutoDownload + + + + diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml new file mode 100644 index 0000000..1eea79f --- /dev/null +++ b/src/gui/Browse.qml @@ -0,0 +1,372 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Item { + width: parent.width; height: parent.height + clip: true + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property bool refreshing: false + + property int buttonHeight: 25 + property int buttonWidth: 103 + property int fontMedium: 11 + + property int scrollY: 0 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + function refresh() { + scrollY = studioListView.contentY; + var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY); + if (currentIndex == -1) { + currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale)); + } + virtualstudio.refreshStudios(currentIndex) + } + + Rectangle { + z: 1 + width: parent.width; height: parent.height + color: "#40000000" + visible: refreshing + MouseArea { + anchors.fill: parent + propagateComposedEvents: false + hoverEnabled: true + preventStealing: true + } + } + + Component { + id: sectionHeading + Rectangle { + color: "transparent" + height: 72 * virtualstudio.uiScale; x: 16 * virtualstudio.uiScale; width: ListView.view.width - (2 * x) + // required property string section: section (for 5.15) + Text { + id: sectionText + //anchors.bottom: parent.bottom + y: 12 * virtualstudio.uiScale + // text: parent.section (for 5.15) + text: section + font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold } + color: textColour + } + Button { + id: createButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: createButton.down ? "#E7E8E8" : "#F2F3F3" + border.width: 1 + border.color: createButton.down ? "#B0B5B5" : "#EAEBEB" + layer.enabled: createButton.hovered && !createButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: "#80A1A1A1" + } + } + onClicked: { virtualstudio.createStudio(); } + anchors.right: filterButton.left + anchors.rightMargin: 16 + anchors.verticalCenter: sectionText.verticalCenter + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Create a Studio" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#DB0A0A" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: section == virtualstudio.logoSection ? true : false + } + Button { + id: filterButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: filterButton.down ? "#E7E8E8" : "#F2F3F3" + border.width: 1 + border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB" + layer.enabled: filterButton.hovered && !filterButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: "#80A1A1A1" + } + } + onClicked: { filterMenu.open(); } + anchors.right: parent.right + anchors.verticalCenter: sectionText.verticalCenter + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Filter Studios" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: section == virtualstudio.logoSection ? true : false + + Popup { + id: filterMenu + y: Math.round(parent.height + 8) + rightMargin: 16 * virtualstudio.uiScale + width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale + modal: false + focus: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: "#F6F8F8" + border.width: 1 + border.color: "#34979797" + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: "#80A1A1A1" + } + } + contentItem: Column { + anchors.fill: parent + CheckBox { + id: inactiveCheckbox + text: qsTr("Show my inactive Studios") + checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked + onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked; + refreshing = true; + refresh(); + } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: inactiveCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" + visible: inactiveCheckbox.checked + } + } + contentItem: Text { + text: inactiveCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing + } + } + CheckBox { + id: selfHostedCheckbox + text: qsTr("Show self-hosted Studios") + checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked + onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked; + refreshing = true; + refresh(); + } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: selfHostedCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" + visible: selfHostedCheckbox.checked + } + } + contentItem: Text { + text: selfHostedCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing + } + } + } + } + } + } + } + + Component { + id: footer + Rectangle { + height: 16 * virtualstudio.uiScale + x: 16 * virtualstudio.uiScale + width: parent.width - (2 * x) + color: backgroundColour + } + } + + ListView { + id: studioListView + x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale + spacing: 16 * virtualstudio.uiScale + header: footer + footer: footer + model: serverModel + clip: true + boundsBehavior: Flickable.StopAtBounds + delegate: Studio { + x: 16 * virtualstudio.uiScale + width: studioListView.width - (2 * x) + serverLocation: location + flagImage: flag + studioName: name + publicStudio: isPublic + manageable: isManageable + available: canConnect + connected: false + } + + section {property: "type"; criteria: ViewSection.FullString; delegate: sectionHeading } + + // Disable momentum scroll + MouseArea { + z: -1 + anchors.fill: parent + onWheel: { + // trackpad + studioListView.contentY -= wheel.pixelDelta.y; + // mouse wheel + studioListView.contentY -= wheel.angleDelta.y; + studioListView.returnToBounds(); + } + } + + Component.onCompleted: { + // Customize scroll properties on different platforms + if (Qt.platform.os == "linux" || Qt.platform.os == "osx" || + Qt.platform.os == "unix" || Qt.platform.os == "windows") { + var scrollBar = Qt.createQmlObject('import QtQuick.Controls 2.12; ScrollBar{}', + studioListView, + "dynamicSnippet1"); + scrollBar.policy = ScrollBar.AlwaysOn; + ScrollBar.vertical = scrollBar; + } + } + } + + Rectangle { + x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale + border.color: "#33979797" + color: backgroundColour + + Button { + id: refreshButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { refreshing = true; refresh() } + anchors.verticalCenter: parent.verticalCenter + x: 16 * virtualstudio.uiScale + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Refresh List" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: aboutButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.showAbout() } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (230 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "About" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: settingsButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: window.state = "settings" + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (119 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Settings" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + + Connections { + target: virtualstudio + // Need to do this to avoid layout issues with our section header. + function onNewScale() { + studioListView.positionViewAtEnd(); + studioListView.positionViewAtBeginning(); + scrollY = studioListView.contentY; + } + function onRefreshFinished(index) { + refreshing = false; + if (index == -1) { + studioListView.contentY = scrollY + } else { + studioListView.positionViewAtIndex(index, ListView.Beginning); + } + } + function onPeriodicRefresh() { refresh() } + } +} diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml new file mode 100644 index 0000000..b449c46 --- /dev/null +++ b/src/gui/Connected.qml @@ -0,0 +1,94 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Item { + width: parent.width; height: parent.height + clip: true + + property bool connecting: false + + property int leftMargin: 16 + property int fontBig: 28 + property int fontMedium: 18 + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 + + Image { + x: parent.width - (49 * virtualstudio.uiScale); y: 16 * virtualstudio.uiScale + width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale + source: "logo.svg" + } + + Text { + id: heading + text: virtualstudio.connectionState + x: leftMargin * virtualstudio.uiScale; y: 34 * virtualstudio.uiScale + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Studio { + x: parent.leftMargin * virtualstudio.uiScale; y: 96 * virtualstudio.uiScale + width: parent.width - (2 * x) + connected: true + serverLocation: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].location : "Germany - Berlin" + flagImage: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].flag : "flags/DE.svg" + studioName: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].name : "Test Studio" + publicStudio: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isPublic : false + manageable: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable : false + available: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].canConnect : false + } + + Image { + id: mic + source: "mic.svg" + x: 80 * virtualstudio.uiScale; y: 250 * virtualstudio.uiScale + width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + } + + Colorize { + anchors.fill: mic + source: mic + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + Image { + id: headphones + source: "headphones.svg" + anchors.horizontalCenter: mic.horizontalCenter + y: 329 * virtualstudio.uiScale + width: 24 * virtualstudio.uiScale; height: 26 * virtualstudio.uiScale + } + + Colorize { + anchors.fill: headphones + source: headphones + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + Text { + x: 120 * virtualstudio.uiScale + text: virtualstudio.audioBackend == "JACK" ? + virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice] + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors.verticalCenter: mic.verticalCenter + color: textColour + } + + Text { + x: 120 * virtualstudio.uiScale + text: virtualstudio.audioBackend == "JACK" ? + virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice] + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors.verticalCenter: headphones.verticalCenter + color: textColour + } + + //43 822 +} diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml new file mode 100644 index 0000000..7356df2 --- /dev/null +++ b/src/gui/FirstLaunch.qml @@ -0,0 +1,139 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Item { + width: parent.width; height: parent.height + clip: true + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" + property string buttonColour: virtualstudio.darkMode ? "#565252" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#6F6C6C" : "#F0F1F1" + property string buttonPressedColour: virtualstudio.darkMode ? "#494646" : "#D8D9D9" + property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF" + property string buttonHoverStroke: virtualstudio.darkMode ? "#777575" : "#DEDFDF" + property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + + Image { + source: "logo.svg" + anchors.horizontalCenter: parent.horizontalCenter + y: 35 * virtualstudio.uiScale + width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + y: 168 * virtualstudio.uiScale + text: "Sign in with a Virtual Studio account?" + font.family: "Poppins" + font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + y: 219 * virtualstudio.uiScale + text: "You'll be able to change your mind later" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + } + + Button { + id: vsButton + background: Rectangle { + radius: 10 * virtualstudio.uiScale + color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke) + layer.enabled: vsButton.hovered && !vsButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + } + } + onClicked: { window.state = "login"; virtualstudio.toVirtualStudio(); } + x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale + Text { + text: "Yes" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#DB0A0A" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + Text { + text: "• Connect to Virtual Studios
• Broadcast on JackTrip Radio
• Apply FX with Soundscapes" + textFormat: Text.StyledText + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + x: parent.width / 2 - (265 * virtualstudio.uiScale); + y: 355 * virtualstudio.uiScale; + width: 230 * virtualstudio.uiScale + padding: 0 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: textColour + } + Image { + source: "JTVS.png" + x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 201.48 * virtualstudio.uiScale; + } + + Button { + id: standardButton + background: Rectangle { + radius: 10 * virtualstudio.uiScale + color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke) + layer.enabled: standardButton.hovered && !standardButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + } + } + onClicked: { window.state = "login"; virtualstudio.toStandard(); } + x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale + Text { + text: "No" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#DB0A0A" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + Image { + source: "JTOriginal.png" + x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale; + } + Text { + text: virtualstudio.psiBuild ? "• Connect via IP address
• Run a local hub server
• The Standard JackTrip experience" : + "• Connect via IP address
• Run a local hub server
• The Classic JackTrip experience" + textFormat: Text.StyledText + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + x: parent.width / 2 + (32 * virtualstudio.uiScale); + y: 355 * virtualstudio.uiScale; + width: 230 * virtualstudio.uiScale + padding: 0 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: textColour + } +} diff --git a/src/gui/JTOriginal.png b/src/gui/JTOriginal.png new file mode 100644 index 0000000..a6c92cd Binary files /dev/null and b/src/gui/JTOriginal.png differ diff --git a/src/gui/JTVS.png b/src/gui/JTVS.png new file mode 100644 index 0000000..bbd5c1d Binary files /dev/null and b/src/gui/JTVS.png differ diff --git a/src/gui/Login.qml b/src/gui/Login.qml new file mode 100644 index 0000000..0ed3fc0 --- /dev/null +++ b/src/gui/Login.qml @@ -0,0 +1,141 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Item { + width: parent.width; height: parent.height + clip: true + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property bool failTextVisible: false + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" + property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" + property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7" + property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" + property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" + property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" + property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" + + onFailTextVisibleChanged: { + authFailedText.visible = failTextVisible; + loginButton.visible = failTextVisible || !virtualstudio.hasRefreshToken; + backButton.visible = failTextVisible || !virtualstudio.hasRefreshToken; + loggingInText.visible = !failTextVisible && virtualstudio.hasRefreshToken; + } + + Image { + id: loginLogo + source: "logo.svg" + x: parent.width / 2 - (150 * virtualstudio.uiScale); y: 110 * virtualstudio.uiScale + width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale + } + + Image { + source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png" + anchors.bottom: loginLogo.bottom + x: parent.width / 2 - (88 * virtualstudio.uiScale) + width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale + } + + Text { + text: "Virtual Studio" + font.family: "Poppins" + font.pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 208 * virtualstudio.uiScale + color: textColour + } + + Text { + id: loggingInText + text: "Logging in..." + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 282 * virtualstudio.uiScale + visible: virtualstudio.hasRefreshToken + color: textColour + } + + Text { + id: authFailedText + text: "Log in failed. Please try again." + font.family: "Poppins" + font.pixelSize: 16 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 272 * virtualstudio.uiScale + visible: failTextVisible + color: textColour + } + + Button { + id: loginButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour) + border.width: loginButton.down ? 1 : 0 + border.color: buttonStroke + layer.enabled: !loginButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + } + } + onClicked: { failTextVisible = false; virtualstudio.login() } + anchors.horizontalCenter: parent.horizontalCenter + y: 321 * virtualstudio.uiScale + width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale + Text { + text: "Sign In" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour) + } + visible: !virtualstudio.hasRefreshToken + } + + Button { + id: backButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour) + border.width: backButton.down ? 1 : 0 + border.color: buttonStroke + layer.enabled: !backButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + } + } + onClicked: { window.state = "start" } + anchors.horizontalCenter: parent.horizontalCenter + y: 401 * virtualstudio.uiScale + width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale + Text { + text: "Back" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour) + } + visible: true + } +} diff --git a/src/gui/Poppins-Bold.ttf b/src/gui/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/src/gui/Poppins-Bold.ttf differ diff --git a/src/gui/Poppins-Regular.ttf b/src/gui/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/src/gui/Poppins-Regular.ttf differ diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml new file mode 100644 index 0000000..0ba2ed8 --- /dev/null +++ b/src/gui/Settings.qml @@ -0,0 +1,318 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Item { + width: parent.width; height: parent.height + clip: true + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property int fontBig: 28 + property int fontMedium: 13 + property int fontSmall: 11 + + property int leftMargin: 48 + property int buttonWidth: 103 + property int buttonHeight: 25 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + Text { + x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale + text: "Settings" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: backendCombo + model: backendComboModel + currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 + onActivated: { virtualstudio.audioBackend = currentText } + x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.selectableBackend + } + + Text { + id: backendLabel + anchors.verticalCenter: backendCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Audio Backend" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.selectableBackend + color: textColour + } + + Text { + id: jackLabel + x: leftMargin * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale) + text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend + 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 + visible: virtualstudio.audioBackend != "JACK" + } + + ComboBox { + id: outputCombo + model: outputComboModel + currentIndex: virtualstudio.outputDevice + onActivated: { virtualstudio.outputDevice = currentIndex } + x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale) + width: backendCombo.width; height: backendCombo.height + visible: virtualstudio.audioBackend != "JACK" + } + + 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 + } + + 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 + } + + Button { + id: refreshButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.refreshDevices() } + x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + Text { + text: "Refresh Device List" + font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Rectangle { + x: leftMargin * virtualstudio.uiScale; y: inputCombo.y + (146 * virtualstudio.uiScale) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale + color: textColour + visible: virtualstudio.audioBackend != "JACK" + } + + ComboBox { + id: bufferCombo + x: backendCombo.x; y: inputCombo.y + (162 * virtualstudio.uiScale) + width: backendCombo.width; height: backendCombo.height + model: bufferComboModel + currentIndex: virtualstudio.bufferSize + onActivated: { virtualstudio.bufferSize = currentIndex } + font.family: "Poppins" + visible: virtualstudio.audioBackend != "JACK" + } + + Text { + anchors.verticalCenter: bufferCombo.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Buffer Size" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.audioBackend != "JACK" + color: textColour + } + + Rectangle { + id: separator + x: leftMargin * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale + y: virtualstudio.audioBackend == "JACK" ? + (virtualstudio.selectableBackend ? backendCombo.y + (48 * virtualstudio.uiScale) : jackLabel.y + (64 * virtualstudio.uiScale)) : bufferCombo.y + (52 * virtualstudio.uiScale) + color: textColour + } + + Slider { + id: scaleSlider + x: backendCombo.x; y: separator.y + (16 * virtualstudio.uiScale) + width: backendCombo.width + from: 1; to: 2; value: virtualstudio.uiScale + onMoved: { virtualstudio.uiScale = value } + } + + Text { + anchors.verticalCenter: scaleSlider.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Scale Interface" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Button { + id: modeButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { window.state = "login"; virtualstudio.toStandard(); } + x: parent.width - (232 * virtualstudio.uiScale); y: scaleSlider.y + (40 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: darkButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; } + x: parent.width -(464 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + anchors.verticalCenter: modeButton.verticalCenter + Text { + text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Text { + anchors.verticalCenter: modeButton.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Change Mode" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: updateChannelCombo + x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (40 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + model: updateChannelComboModel + currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1 + onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" } + font.family: "Poppins" + visible: !virtualstudio.noUpdater + } + + Text { + anchors.verticalCenter: updateChannelCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Update Channel" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: !virtualstudio.noUpdater + } + + Text { + x: leftMargin * virtualstudio.uiScale; y: parent.height - (75 * virtualstudio.uiScale) + text: "JackTrip version " + virtualstudio.versionString + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + color: textColour + } + + Button { + id: logoutButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { window.state = "login"; virtualstudio.logout() } + x: parent.width - ((16 + buttonWidth) * virtualstudio.uiScale) + y: virtualstudio.noUpdater ? modeButton.y + (46 * virtualstudio.uiScale) : updateChannelCombo.y + (46 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Log Out" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Rectangle { + x: 0; y: parent.height - (36 * virtualstudio.uiScale) + width: parent.width; height: (36 * virtualstudio.uiScale) + border.color: "#33979797" + color: backgroundColour + + Button { + id: cancelButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { window.state = "browse"; virtualstudio.revertSettings() } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (230 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Cancel" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: saveButton + 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() } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (119 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Save" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } +} diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml new file mode 100644 index 0000000..d7f53e6 --- /dev/null +++ b/src/gui/Setup.qml @@ -0,0 +1,505 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontBig: 28 + property int fontMedium: 13 + property int fontSmall: 11 + + property int leftMargin: 48 + property int buttonWidth: 103 + property int buttonHeight: 25 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string saveButtonShadow: "#80A1A1A1" + property string saveButtonBackgroundColour: "#F2F3F3" + property string saveButtonPressedColour: "#E7E8E8" + property string saveButtonStroke: "#EAEBEB" + property string saveButtonPressedStroke: "#B0B5B5" + property string warningText: "#DB0A0A" + property string saveButtonText: "#DB0A0A" + property string checkboxStroke: "#0062cc" + property string checkboxPressedStroke: "#007AFF" + + property bool currShowWarnings: virtualstudio.showWarnings + property string warningScreen: virtualstudio.showWarnings ? "ethernet" : "acknowledged" + + Item { + id: ethernetWarningItem + width: parent.width; height: parent.height + visible: warningScreen == "ethernet" + + Image { + id: ethernetWarningLogo + source: "ethernet.png" + width: 179 + height: 128 + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + } + + Colorize { + anchors.fill: ethernetWarningLogo + source: ethernetWarningLogo + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + Text { + id: ethernetWarningHeader + text: "Connect via Wired Ethernet" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ethernetWarningLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: ethernetWarningSubheader1 + text: "JackTrip works best when you connect directly to your router via wired ethernet." + 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: ethernetWarningHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: ethernetWarningSubheader2 + text: "WiFi works OK for some people, but you will experience higher latency and audio glitches." + 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: ethernetWarningSubheader1.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + } + + Button { + id: okButtonEthernet + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonEthernet.down ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: saveButtonShadow + } + } + onClicked: { warningScreen = "headphones" } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "OK" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + CheckBox { + id: showEthernetWarningCheckbox + checked: currShowWarnings + text: qsTr("Show warnings again next time") + anchors.right: okButtonEthernet.left + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.verticalCenter: okButtonEthernet.verticalCenter + onClicked: { currShowWarnings = showEthernetWarningCheckbox.checkState == Qt.Checked } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showEthernetWarningCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + visible: showEthernetWarningCheckbox.checked + } + } + contentItem: Text { + text: showEthernetWarningCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showEthernetWarningCheckbox.indicator.width + showEthernetWarningCheckbox.spacing + color: textColour + } + } + } + + Item { + id: headphoneWarningItem + width: parent.width; height: parent.height + visible: warningScreen == "headphones" + + Image { + id: headphoneWarningLogo + source: "headphones.svg" + sourceSize: Qt.size( img.sourceSize.width*5, img.sourceSize.height*5 ) + Image { + id: img + source: parent.source + width: 0 + height: 0 + } + width: 118 + height: 128 + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + } + + Colorize { + anchors.fill: headphoneWarningLogo + source: headphoneWarningLogo + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + Text { + id: headphoneWarningHeader + text: "Use Wired Headphones" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: headphoneWarningLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: headphoneWarningSubheader1 + text: "JackTrip requires the use of wired headphones." + 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: headphoneWarningHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: headphoneWarningSubheader2 + text: "Using speakers can cause loud feedback loops." + 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: headphoneWarningSubheader1.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + } + + Text { + id: headphoneWarningSubheader3 + text: "Wireless headphones add way too much latency." + 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: headphoneWarningSubheader2.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + } + + Button { + id: okButtonHeadphones + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonHeadphones.down ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: saveButtonShadow + } + } + onClicked: { virtualstudio.showWarnings = currShowWarnings; warningScreen = "acknowledged" } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "OK" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + CheckBox { + id: showHeadphonesWarningCheckbox + checked: currShowWarnings + text: qsTr("Show warnings again next time") + anchors.right: okButtonHeadphones.left + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.verticalCenter: okButtonHeadphones.verticalCenter + onClicked: { currShowWarnings = showHeadphonesWarningCheckbox.checkState == Qt.Checked } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showHeadphonesWarningCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + visible: showHeadphonesWarningCheckbox.checked + } + } + contentItem: Text { + text: showHeadphonesWarningCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showHeadphonesWarningCheckbox.indicator.width + showHeadphonesWarningCheckbox.spacing + color: textColour + } + } + } + + Item { + id: setupItem + width: parent.width; height: parent.height + visible: warningScreen == "acknowledged" + + Text { + x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale + text: "Choose your audio devices" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: backendCombo + model: backendComboModel + currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 + onActivated: { virtualstudio.audioBackend = currentText } + x: 234 * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.selectableBackend + } + + Text { + id: backendLabel + anchors.verticalCenter: backendCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Audio Backend" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.selectableBackend + color: textColour + } + + Text { + id: jackLabel + x: leftMargin * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale) + text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend + color: textColour + } + + ComboBox { + id: inputCombo + model: inputComboModel + currentIndex: virtualstudio.inputDevice + onActivated: { virtualstudio.inputDevice = currentIndex } + x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 198 : 150) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + } + + ComboBox { + id: outputCombo + model: outputComboModel + currentIndex: virtualstudio.outputDevice + onActivated: { virtualstudio.outputDevice = currentIndex } + x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale) + width: backendCombo.width; height: backendCombo.height + visible: virtualstudio.audioBackend != "JACK" + } + + Text { + id: inputLabel + 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 + } + + Text { + id: outputLabel + 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 + } + + Button { + id: refreshButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.refreshDevices() } + x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + Text { + text: "Refresh Device List" + font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Text { + anchors.left: outputLabel.left + anchors.right: outputCombo.right + anchors.leftMargin: 16 * virtualstudio.uiScale + anchors.rightMargin: 16 * virtualstudio.uiScale + y: inputCombo.y + (160 * 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 + wrapMode: Text.WordWrap + color: warningText + font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + visible: Qt.platform.os == "windows" && virtualstudio.audioBackend != "JACK" + } + + Button { + id: saveButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: saveButton.down ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: saveButton.hovered && !saveButton.down + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: saveButtonShadow + } + } + onClicked: { window.state = "browse"; virtualstudio.applySettings() } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Save 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 + } + } + + CheckBox { + id: showAgainCheckbox + checked: virtualstudio.showDeviceSetup + text: qsTr("Ask again next time") + anchors.right: saveButton.left + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.verticalCenter: saveButton.verticalCenter + onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showAgainCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke + visible: showAgainCheckbox.checked + } + } + + contentItem: Text { + text: showAgainCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing + color: textColour + } + } + } +} diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml new file mode 100644 index 0000000..f2004f5 --- /dev/null +++ b/src/gui/Studio.qml @@ -0,0 +1,210 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Rectangle { + width: 664; height: 83 * virtualstudio.uiScale + radius: 6 * virtualstudio.uiScale + color: backgroundColour + border.width: 0.3 + border.color: "#40979797" + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 1 * virtualstudio.uiScale + verticalOffset: 1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + } + + property string serverLocation: "Germany - Berlin" + property string flagImage: "flags/DE.svg" + property string studioName: "Test Studio" + property bool publicStudio: false + property bool manageable: false + property bool available: true + property bool connected: false + + property int leftMargin: 81 + property int topMargin: 13 + + property real fontBig: 18 + property real fontMedium: 11 + property real fontSmall: 8 + + property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" + property string joinColour: virtualstudio.darkMode ? (connected ? "#FCB6B6" : "#E2EBE0") : (connected ? "#FCB6B6" : "#C4F4BE") + property string joinHoverColour: virtualstudio.darkMode ? (connected ? "#D49696" : "#BAC7B8") : (connected ? "#E3A4A4" : "#B0DCAB") + property string joinPressedColour: virtualstudio.darkMode ? (connected ? "#F2AEAE" : "#D8E2D6") : (connected ? "#EFADAD" : "#BAE8B5") + property string joinStroke: virtualstudio.darkMode ? (connected ? "#A65959" : "#748F70") : (connected ? "#C95E5E" : "#5DB752") + property string manageColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB" + property string manageHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3" + property string managePressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB" + property string manageStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494" + + Rectangle { + id: shadow + anchors.fill: parent + color: "transparent" + radius: 6 + } + + DropShadow { + horizontalOffset: -1 * virtualstudio.uiScale + verticalOffset: -1 * virtualstudio.uiScale + radius: 8.0 * virtualstudio.uiScale + samples: 17 + color: shadowColour + source: shadow + } + + Rectangle { + width: 12 * virtualstudio.uiScale; height: parent.height + radius: width / 2 + color: available ? "#0C1424" : "#B3B3B3" + } + + Image { + source: available ? "wedge.svg" : "wedge_inactive.svg" + x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale + } + + Image { + source: "logo.svg" + x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale + } + + Rectangle { + x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale + width: 32 * virtualstudio.uiScale; height: width + radius: width / 2 + color: available ? "#0C1424" : "#B3B3B3" + } + + Image { + id: flag + source: flagImage + x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale + width: 40 * virtualstudio.uiScale; height: width / 4 * 3 + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.effect: OpacityMask { + maskSource: mask + } + } + + Rectangle { + id: mask + x: 0 ; y: 0 ; width: flag.width; height: flag.height + visible: false + color: "#00000000" + Rectangle { + x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale + width:24 * virtualstudio.uiScale; height: width + radius: width / 2 + } + } + + Text { + x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale; + width: manageable ? parent.width - (233 * virtualstudio.uiScale) : parent.width - (156 * virtualstudio.uiScale) + text: studioName + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + elide: Text.ElideRight + color: textColour + } + + Rectangle { + id: publicRect + x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale + width: 14 * virtualstudio.uiScale; height: width + radius: 2 * virtualstudio.uiScale + color: publicStudio ? "#0095FF" : "#FF9800" + Image { + source: publicStudio ? "public.svg" : "private.svg" + x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width + } + } + + Text { + anchors.verticalCenter: publicRect.verticalCenter + x: (leftMargin + 22) * virtualstudio.uiScale + width: manageable ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) + text: publicStudio ? "Public hub studio in " + serverLocation : "Private hub studio in " + serverLocation + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + elide: Text.ElideRight + color: textColour + } + + Button { + id: joinButton + x: manageable ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale) + y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: joinButton.down ? joinPressedColour : (joinButton.hovered ? joinHoverColour : joinColour) + border.width: joinButton.down ? 1 : 0 + border.color: joinStroke + } + visible: connected || canConnect || canStart + onClicked: { + if (!connected) { + window.state = "connected"; + virtualstudio.connectToStudio(index); + } else { + virtualstudio.disconnect(); + } + } + Image { + width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: connected ? "leave.svg" : "join.svg" + } + } + + Text { + anchors.horizontalCenter: joinButton.horizontalCenter + y: 56 * virtualstudio.uiScale + text: connected ? "Leave" : available ? "Join" : "Start" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + visible: connected || canConnect || canStart + color: textColour + } + + Button { + id: manageButton + x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale + width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour) + border.width: manageButton.down ? 1 : 0 + border.color: manageStroke + } + onClicked: { + if (!connected) { + virtualstudio.manageStudio(index) + } else { + virtualstudio.manageStudio(-1) + } + } + visible: manageable + Image { + width: 20 * virtualstudio.uiScale; height: width + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: "cog.svg" + } + } + + Text { + anchors.horizontalCenter: manageButton.horizontalCenter + y: 56 * virtualstudio.uiScale + text: "Manage" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: manageable + color: textColour + } +} diff --git a/src/gui/cog.svg b/src/gui/cog.svg new file mode 100644 index 0000000..ba5645a --- /dev/null +++ b/src/gui/cog.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/ethernet.png b/src/gui/ethernet.png new file mode 100644 index 0000000..c911dc4 Binary files /dev/null and b/src/gui/ethernet.png differ diff --git a/src/gui/flags/AE.svg b/src/gui/flags/AE.svg new file mode 100644 index 0000000..59ddafd --- /dev/null +++ b/src/gui/flags/AE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/AU.svg b/src/gui/flags/AU.svg new file mode 100644 index 0000000..f91b013 --- /dev/null +++ b/src/gui/flags/AU.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/BE.svg b/src/gui/flags/BE.svg new file mode 100644 index 0000000..cc1b013 --- /dev/null +++ b/src/gui/flags/BE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/BR.svg b/src/gui/flags/BR.svg new file mode 100644 index 0000000..f4dbb02 --- /dev/null +++ b/src/gui/flags/BR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/CA.svg b/src/gui/flags/CA.svg new file mode 100644 index 0000000..457d316 --- /dev/null +++ b/src/gui/flags/CA.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/CH.svg b/src/gui/flags/CH.svg new file mode 100644 index 0000000..498b7d1 --- /dev/null +++ b/src/gui/flags/CH.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/DE.svg b/src/gui/flags/DE.svg new file mode 100644 index 0000000..df0775b --- /dev/null +++ b/src/gui/flags/DE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/FR.svg b/src/gui/flags/FR.svg new file mode 100644 index 0000000..9f02836 --- /dev/null +++ b/src/gui/flags/FR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/GB.svg b/src/gui/flags/GB.svg new file mode 100644 index 0000000..4ada58a --- /dev/null +++ b/src/gui/flags/GB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/HK.svg b/src/gui/flags/HK.svg new file mode 100644 index 0000000..284a722 --- /dev/null +++ b/src/gui/flags/HK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/ID.svg b/src/gui/flags/ID.svg new file mode 100644 index 0000000..45d3745 --- /dev/null +++ b/src/gui/flags/ID.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/IT.svg b/src/gui/flags/IT.svg new file mode 100644 index 0000000..17b1314 --- /dev/null +++ b/src/gui/flags/IT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/JP.svg b/src/gui/flags/JP.svg new file mode 100644 index 0000000..92eb885 --- /dev/null +++ b/src/gui/flags/JP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/RO.svg b/src/gui/flags/RO.svg new file mode 100644 index 0000000..fabf12e --- /dev/null +++ b/src/gui/flags/RO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/SE.svg b/src/gui/flags/SE.svg new file mode 100644 index 0000000..7ec1787 --- /dev/null +++ b/src/gui/flags/SE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/SG.svg b/src/gui/flags/SG.svg new file mode 100644 index 0000000..c374c47 --- /dev/null +++ b/src/gui/flags/SG.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/TW.svg b/src/gui/flags/TW.svg new file mode 100644 index 0000000..c3660f1 --- /dev/null +++ b/src/gui/flags/TW.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/US.svg b/src/gui/flags/US.svg new file mode 100644 index 0000000..dc427e7 --- /dev/null +++ b/src/gui/flags/US.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/flags/ZA.svg b/src/gui/flags/ZA.svg new file mode 100644 index 0000000..1b294c9 --- /dev/null +++ b/src/gui/flags/ZA.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/headphones.svg b/src/gui/headphones.svg new file mode 100644 index 0000000..fa3a213 --- /dev/null +++ b/src/gui/headphones.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/jacktrip white.png b/src/gui/jacktrip white.png new file mode 100644 index 0000000..7ba69b0 Binary files /dev/null and b/src/gui/jacktrip white.png differ diff --git a/src/gui/jacktrip.png b/src/gui/jacktrip.png new file mode 100644 index 0000000..c4b998a Binary files /dev/null and b/src/gui/jacktrip.png differ diff --git a/src/gui/join.svg b/src/gui/join.svg new file mode 100644 index 0000000..a52a534 --- /dev/null +++ b/src/gui/join.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/leave.svg b/src/gui/leave.svg new file mode 100644 index 0000000..c44f7e7 --- /dev/null +++ b/src/gui/leave.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/logo.svg b/src/gui/logo.svg new file mode 100644 index 0000000..508c81b --- /dev/null +++ b/src/gui/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/mic.svg b/src/gui/mic.svg new file mode 100644 index 0000000..2568def --- /dev/null +++ b/src/gui/mic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/gui/private.svg b/src/gui/private.svg new file mode 100644 index 0000000..c08eb3d --- /dev/null +++ b/src/gui/private.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/public.svg b/src/gui/public.svg new file mode 100644 index 0000000..1407d57 --- /dev/null +++ b/src/gui/public.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index 3330d25..b5a5c3f 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -34,7 +34,10 @@ #include #include "about.h" -#ifdef NO_JTVS +#ifndef NO_VS +#include "virtualstudio.h" +#endif +#ifdef PSI #include "ui_qjacktrip_novs.h" #else #include "ui_qjacktrip.h" @@ -54,7 +57,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) : QMainWindow(parent) -#ifdef NO_JTVS +#ifdef PSI , m_ui(new Ui::QJackTrip) #else , m_ui(new Ui::QJackTripVS) @@ -72,10 +75,6 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) { m_ui->setupUi(this); - QCoreApplication::setOrganizationName(QStringLiteral("jacktrip")); - QCoreApplication::setOrganizationDomain(QStringLiteral("jacktrip.org")); - QCoreApplication::setApplicationName(QStringLiteral("JackTrip")); - // Set up our debug window, and relay everything to our real cout. std::cout.rdbuf(m_debugDialog->getOutputStream()->rdbuf()); std::cerr.rdbuf(m_debugDialog->getOutputStream(1)->rdbuf()); @@ -108,6 +107,10 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) About about(this); about.exec(); }); +#ifndef NO_VS + connect(m_ui->vsModeButton, &QPushButton::clicked, this, + &QJackTrip::virtualStudioMode); +#endif connect(m_ui->autoPatchComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [=]() { if (m_ui->autoPatchComboBox->currentIndex() == CLIENTFOFI @@ -234,6 +237,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) m_ui->autoPatchGroupBox->setVisible(false); m_ui->requireAuthGroupBox->setVisible(false); m_ui->backendWarningLabel->setVisible(false); + m_ui->vsModeButton->setVisible(false); #ifdef RT_AUDIO connect(m_ui->backendComboBox, QOverload::of(&QComboBox::currentIndexChanged), @@ -324,7 +328,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) "JACK was not found. This means that only the RtAudio backend is available " "and that JackTrip cannot be run in hub server mode."); -#ifdef NO_JTVS +#ifdef PSI QSettings settings; settings.beginGroup(QStringLiteral("Audio")); if (!settings.value(QStringLiteral("HideJackWarning"), false).toBool()) { @@ -360,7 +364,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) settings.setValue(QStringLiteral("UsingFallback"), false); } settings.endGroup(); -#endif // NO_JTVS +#endif // PSI #else // RT_AUDIO QMessageBox msgBox; msgBox.setText( @@ -415,6 +419,37 @@ void QJackTrip::resizeEvent(QResizeEvent* event) m_ui->authDisclaimerLabel->setMinimumHeight(rect.height()); } +void QJackTrip::showEvent(QShowEvent* event) +{ + // We need to wait to load geometry until here rather than with our other settings. + // If we don't, the window geometry will be improperly set on macOS whenever the + // VirtualStudio window is shown first. + QMainWindow::showEvent(event); + if (m_firstShow) { + QSettings settings; + settings.beginGroup(QStringLiteral("Window")); + QByteArray geometry = settings.value(QStringLiteral("Geometry")).toByteArray(); + if (geometry.size() > 0) { + restoreGeometry(geometry); + } else { + // Because of hidden elements in our dialog window, it's vertical size in the + // creator is getting rediculous. Set it to something sensible by default if + // this is our first load. + this->resize(QSize(this->size().height(), 600)); + } + settings.endGroup(); + m_firstShow = false; + } +} + +#ifndef NO_VS +void QJackTrip::setVs(QSharedPointer vs) +{ + m_vs = vs; + m_ui->vsModeButton->setVisible(!m_vs.isNull()); +} +#endif + void QJackTrip::processFinished() { if (!m_jackTripRunning) { @@ -444,7 +479,7 @@ void QJackTrip::processError(const QString& errorMessage) { QMessageBox msgBox; if (errorMessage == QLatin1String("Peer Stopped")) { - // Report the other end quitting as a regular occurance rather than an error. + // Report the other end quitting as a regular occurrence rather than an error. msgBox.setText(errorMessage); msgBox.setWindowTitle(QStringLiteral("Disconnected")); } else { @@ -670,6 +705,7 @@ void QJackTrip::resetOptions() m_ui->realTimeCheckBox->setChecked(true); m_ui->ioStatsCheckBox->setChecked(false); m_ui->ioStatsSpinBox->setValue(1); + m_ui->verboseCheckBox->setChecked(false); saveSettings(); } @@ -943,6 +979,14 @@ void QJackTrip::exit() } } +#ifndef NO_VS +void QJackTrip::virtualStudioMode() +{ + this->hide(); + m_vs->show(); +} +#endif + int QJackTrip::findTab(const QString& tabName) { for (int i = 0; i < m_ui->optionsTabWidget->count(); i++) { @@ -1177,18 +1221,6 @@ void QJackTrip::loadSettings() m_ui->outClientsSpinBox->setValue( settings.value(QStringLiteral("Clients"), 1).toInt()); settings.endGroup(); - - settings.beginGroup(QStringLiteral("Window")); - QByteArray geometry = settings.value(QStringLiteral("Geometry")).toByteArray(); - if (geometry.size() > 0) { - restoreGeometry(geometry); - } else { - // Because of hidden elements in our dialog window, it's vertical size in the - // creator is getting rediculous. Set it to something sensible by default if this - // is our first load. - this->resize(QSize(this->size().width(), 600)); - } - settings.endGroup(); } void QJackTrip::saveSettings() diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h index 27528a7..3410d65 100644 --- a/src/gui/qjacktrip.h +++ b/src/gui/qjacktrip.h @@ -51,13 +51,17 @@ namespace Ui { -#ifdef NO_JTVS +#ifdef PSI class QJackTrip; #else class QJackTripVS; #endif } // namespace Ui +#ifndef NO_VS +class VirtualStudio; +#endif + class QJackTrip : public QMainWindow { Q_OBJECT @@ -68,6 +72,12 @@ class QJackTrip : public QMainWindow void closeEvent(QCloseEvent* event) override; void resizeEvent(QResizeEvent* event) override; + void showEvent(QShowEvent* event) override; + +#ifndef NO_VS + enum uiModeT{UNSET, VIRTUAL_STUDIO, STANDARD}; + void setVs(QSharedPointer vs); +#endif signals: void signalExit(); @@ -88,6 +98,9 @@ class QJackTrip : public QMainWindow void start(); void stop(); void exit(); +#ifndef NO_VS + void virtualStudioMode(); +#endif private: enum runTypeT { P2P_CLIENT, P2P_SERVER, HUB_CLIENT, HUB_SERVER }; @@ -109,7 +122,7 @@ class QJackTrip : public QMainWindow QString commandLineFromCurrentOptions(); void showCommandLineMessageBox(); -#ifdef NO_JTVS +#ifdef PSI QScopedPointer m_ui; #else QScopedPointer m_ui; @@ -132,7 +145,11 @@ class QJackTrip : public QMainWindow QLabel m_autoQueueIndicator; int m_argc; bool m_hideWarning; + bool m_firstShow = true; +#ifndef NO_VS + QSharedPointer m_vs; +#endif #ifdef __APPLE__ NoNap m_noNap; #endif diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index 179c85a..ad8f864 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -4,4 +4,50 @@ about.png icon.png + + vs.qml + FirstLaunch.qml + Login.qml + Studio.qml + Browse.qml + Settings.qml + Connected.qml + Setup.qml + logo.svg + wedge.svg + wedge_inactive.svg + private.svg + public.svg + join.svg + leave.svg + cog.svg + mic.svg + ethernet.png + headphones.svg + jacktrip.png + jacktrip white.png + JTOriginal.png + JTVS.png + flags/AE.svg + flags/AU.svg + flags/BE.svg + flags/BR.svg + flags/CA.svg + flags/CH.svg + flags/DE.svg + flags/FR.svg + flags/GB.svg + flags/HK.svg + flags/ID.svg + flags/IT.svg + flags/JP.svg + flags/RO.svg + flags/SE.svg + flags/SG.svg + flags/TW.svg + flags/US.svg + flags/ZA.svg + Poppins-Bold.ttf + Poppins-Regular.ttf + diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui index 52dc336..9044e47 100644 --- a/src/gui/qjacktrip.ui +++ b/src/gui/qjacktrip.ui @@ -308,6 +308,13 @@ To connect to a hub server you need to run as a hub client. + + + + Virtual Studio Mode + + + @@ -826,6 +833,9 @@ play from this machine. (Available in client fan out/in and full mix modes.) 2 + + 999 + 4 @@ -1470,6 +1480,12 @@ for better quality at the expense of latency. Set the broadcast queue buffer length, in packet size. + + 2 + + + 999 + 8 @@ -1811,6 +1827,7 @@ and wetness is the essence of beauty. keyBrowse credsEdit credsBrowse + vsModeButton aboutButton clientNameEdit remoteNameEdit diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc new file mode 100644 index 0000000..179c85a --- /dev/null +++ b/src/gui/qjacktrip_novs.qrc @@ -0,0 +1,7 @@ + + + about@2x.png + about.png + icon.png + + diff --git a/src/gui/qjacktrip_novs.ui b/src/gui/qjacktrip_novs.ui index 6e6ce34..7aaced2 100644 --- a/src/gui/qjacktrip_novs.ui +++ b/src/gui/qjacktrip_novs.ui @@ -572,6 +572,13 @@ play from this machine. (Available in client fan out/in and full mix modes.) + + + + &Virtual Studio Mode + + + @@ -977,6 +984,9 @@ play from this machine. (Available in client fan out/in and full mix modes.) 2 + + 999 + 4 @@ -1470,6 +1480,12 @@ for better quality at the expense of latency. Set the broadcast queue buffer length, in packet size. + + 2 + + + 999 + 8 @@ -1814,6 +1830,7 @@ and wetness is the essence of beauty. authCheckBox usernameEdit passwordEdit + vsModeButton aboutButton clientNameEdit remoteNameEdit diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp new file mode 100644 index 0000000..93df669 --- /dev/null +++ b/src/gui/virtualstudio.cpp @@ -0,0 +1,1223 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file virtualstudio.cpp + * \author Aaron Wyatt + * \date March 2022 + */ + +#include "virtualstudio.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../jacktrip_globals.h" +#include "about.h" +#include "qjacktrip.h" + +#ifdef USE_WEAK_JACK +#include "weak_libjack.h" +#endif +#ifdef RT_AUDIO +#include "RtAudio.h" +#endif + +#ifdef _WIN32 +#include +#endif + +VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) + : QObject(parent), m_showFirstRun(firstRun) +{ + QSettings settings; + m_updateChannel = + settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower(); + settings.beginGroup(QStringLiteral("VirtualStudio")); + m_refreshToken = settings.value(QStringLiteral("RefreshToken"), "").toString(); + m_userId = settings.value(QStringLiteral("UserId"), "").toString(); + m_uiScale = settings.value(QStringLiteral("UiScale"), 1).toFloat(); + m_darkMode = settings.value(QStringLiteral("DarkMode"), false).toBool(); + m_showInactive = settings.value(QStringLiteral("ShowInactive"), false).toBool(); + m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), false).toBool(); + m_showDeviceSetup = settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool(); + m_showWarnings = settings.value(QStringLiteral("ShowWarnings"), true).toBool(); + settings.endGroup(); + m_previousUiScale = m_uiScale; + + // Load our font for our qml interface + QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf")); + QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf")); + + connect(&m_view, &VsQuickView::windowClose, this, &VirtualStudio::exit); + + // Set our font scaling to convert points to pixels + m_fontScale = 4.0 / 3.0; + +#ifdef RT_AUDIO + settings.beginGroup(QStringLiteral("Audio")); + m_useRtAudio = settings.value(QStringLiteral("Backend"), 0).toInt() == 1; + m_inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); + m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); + m_bufferSize = settings.value(QStringLiteral("BufferSize"), 128).toInt(); + settings.endGroup(); + m_previousBuffer = m_bufferSize; + refreshDevices(); + m_previousInput = m_inputDevice; + m_previousOutput = m_outputDevice; +#else + m_selectableBackend = false; + + // Set our combo box models to an empty list to avoid a reference error + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputComboModel"), + QVariant::fromValue(QStringList(QLatin1String("")))); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputComboModel"), + QVariant::fromValue(QStringList(QLatin1String("")))); +#endif + +#ifdef USE_WEAK_JACK + // Check if Jack is available + if (have_libjack() != 0) { +#ifdef RT_AUDIO + m_useRtAudio = true; + m_selectableBackend = false; +#else + // TODO: Handle this more gracefully, even if it's an unlikely scenario + qFatal("JACK not found and not built with RtAudio support."); +#endif // RT_AUDIO + } +#endif // USE_WEAK_JACK +#ifdef RT_AUDIO + m_previousUseRtAudio = m_useRtAudio; +#endif + + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("bufferComboModel"), QVariant::fromValue(m_bufferOptions)); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("updateChannelComboModel"), + QVariant::fromValue(m_updateChannelOptions)); + m_view.engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"), + this); + m_view.engine()->rootContext()->setContextProperty(QStringLiteral("serverModel"), + QVariant::fromValue(m_servers)); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("backendComboModel"), + QVariant::fromValue(QStringList() + << QStringLiteral("JACK") << QStringLiteral("RtAudio"))); + m_view.setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml"))); + m_view.setMinimumSize(QSize(594, 519)); + // m_view.setMaximumSize(QSize(696, 577)); + m_view.resize(696 * m_uiScale, 577 * m_uiScale); + + // Connect our timers + connect(&m_startTimer, &QTimer::timeout, this, &VirtualStudio::checkForHostname); + connect(&m_retryPeriodTimer, &QTimer::timeout, this, &VirtualStudio::endRetryPeriod); + connect(&m_refreshTimer, &QTimer::timeout, this, [&]() { + m_refreshMutex.lock(); + if (m_allowRefresh) { + m_refreshMutex.unlock(); + emit periodicRefresh(); + } else { + m_refreshMutex.unlock(); + } + }); +} + +void VirtualStudio::setStandardWindow(QSharedPointer window) +{ + m_standardWindow = window; +} + +void VirtualStudio::show() +{ + if (m_checkSsl) { + // Check our available SSL version + QString sslVersion = QSslSocket::sslLibraryVersionString(); + std::cout << "SSL Library: " << sslVersion.toStdString() << std::endl; + if (sslVersion.isEmpty()) { + QMessageBox msgBox; + msgBox.setText( + QStringLiteral("OpenSSL was not found. You will not be able to connect " + "to the Virtual Studio server.")); + msgBox.setWindowTitle(QStringLiteral("SSL Error")); + msgBox.exec(); + } + m_checkSsl = false; + } + + if (!m_showFirstRun) { + toVirtualStudio(); + } + m_view.show(); +} + +bool VirtualStudio::showFirstRun() +{ + return m_showFirstRun; +} + +bool VirtualStudio::hasRefreshToken() +{ + return !m_refreshToken.isEmpty(); +} + +QString VirtualStudio::versionString() +{ + return QLatin1String(gVersion); +} + +QString VirtualStudio::logoSection() +{ + return m_logoSection; +} + +bool VirtualStudio::selectableBackend() +{ + return m_selectableBackend; +} + +QString VirtualStudio::audioBackend() +{ + return m_useRtAudio ? QStringLiteral("RtAudio") : QStringLiteral("JACK"); +} + +void VirtualStudio::setAudioBackend(const QString& backend) +{ + if (!m_selectableBackend) { + return; + } + m_useRtAudio = (backend == QStringLiteral("RtAudio")); + emit audioBackendChanged(); +} + +int VirtualStudio::inputDevice() +{ +#ifdef RT_AUDIO + if (m_useRtAudio) { + int index = m_inputDeviceList.indexOf(m_inputDevice); + return index >= 0 ? index : 0; + } +#endif + return 0; +} + +void VirtualStudio::setInputDevice([[maybe_unused]] int device) +{ + if (!m_useRtAudio) { + return; + } +#ifdef RT_AUDIO + m_inputDevice = m_inputDeviceList.at(device); +#endif +} + +int VirtualStudio::outputDevice() +{ +#ifdef RT_AUDIO + if (m_useRtAudio) { + int index = m_outputDeviceList.indexOf(m_outputDevice); + return index >= 0 ? index : 0; + } +#endif + return 0; +} + +void VirtualStudio::setOutputDevice([[maybe_unused]] int device) +{ + if (!m_useRtAudio) { + return; + } +#ifdef RT_AUDIO + m_outputDevice = m_outputDeviceList.at(device); +#endif +} + +int VirtualStudio::bufferSize() +{ +#ifdef RT_AUDIO + if (m_useRtAudio) { + int index = m_bufferOptions.indexOf(QString::number(m_bufferSize)); + // It shouldn't be possible that our buffer size doesn't exists + // but default to 128 if something goes wrong. + return index >= 0 ? index : m_bufferOptions.indexOf(QStringLiteral("128")); + } +#endif + return 3; +} + +void VirtualStudio::setBufferSize([[maybe_unused]] int index) +{ + if (!m_useRtAudio) { + return; + } +#ifdef RT_AUDIO + m_bufferSize = m_bufferOptions.at(index).toInt(); +#endif +} + +int VirtualStudio::currentStudio() +{ + return m_currentStudio; +} + +QString VirtualStudio::connectionState() +{ + return m_connectionState; +} + +QString VirtualStudio::updateChannel() +{ + return m_updateChannel; +} + +void VirtualStudio::setUpdateChannel(const QString& channel) +{ + m_updateChannel = channel; + QSettings settings; + settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel); + emit updateChannelChanged(); +} + +bool VirtualStudio::showInactive() +{ + return m_showInactive; +} + +void VirtualStudio::setShowInactive(bool inactive) +{ + m_showInactive = inactive; + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("ShowInactive"), m_showInactive); + settings.endGroup(); +} + +bool VirtualStudio::showSelfHosted() +{ + return m_showSelfHosted; +} + +void VirtualStudio::setShowSelfHosted(bool selfHosted) +{ + m_showSelfHosted = selfHosted; + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted); + settings.endGroup(); +} + +bool VirtualStudio::showDeviceSetup() +{ + return m_showDeviceSetup; +} + +void VirtualStudio::setShowDeviceSetup(bool show) +{ + m_showDeviceSetup = show; +} + +bool VirtualStudio::showWarnings() +{ + return m_showWarnings; +} + +void VirtualStudio::setShowWarnings(bool show) +{ + m_showWarnings = show; + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings); + settings.endGroup(); + emit showWarningsChanged(); +} + +float VirtualStudio::fontScale() +{ + return m_fontScale; +} + +float VirtualStudio::uiScale() +{ + return m_uiScale; +} + +void VirtualStudio::setUiScale(float scale) +{ + m_uiScale = scale; + emit uiScaleChanged(); +} + +bool VirtualStudio::darkMode() +{ + return m_darkMode; +} + +void VirtualStudio::setDarkMode(bool dark) +{ + m_darkMode = dark; + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("DarkMode"), m_darkMode); + settings.endGroup(); + emit darkModeChanged(); +} + +bool VirtualStudio::noUpdater() +{ +#ifdef NO_UPDATER + return true; +#else + return false; +#endif +} + +bool VirtualStudio::psiBuild() +{ +#ifdef PSI + return true; +#else + return false; +#endif +} + +void VirtualStudio::toStandard() +{ + if (!m_standardWindow.isNull()) { + m_view.hide(); + m_standardWindow->show(); + } + QSettings settings; + settings.setValue(QStringLiteral("UiMode"), QJackTrip::STANDARD); + m_refreshTimer.stop(); + + if (m_showFirstRun) { + m_showFirstRun = false; + emit showFirstRunChanged(); + } +} + +void VirtualStudio::toVirtualStudio() +{ + if (!m_refreshToken.isEmpty()) { + // Attempt to refresh our virtual studio auth token + setupAuthenticator(); + + // Something about this is required for refreshing auth tokens: + // https://bugreports.qt.io/browse/QTBUG-84866 + m_authenticator->setModifyParametersFunction([](QAbstractOAuth2::Stage stage, + QVariantMap* parameters) { + if (stage == QAbstractOAuth2::Stage::RequestingAccessToken) { + QByteArray code = parameters->value(QStringLiteral("code")).toByteArray(); + (*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code); + } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) { + parameters->insert(QStringLiteral("audience"), + QStringLiteral("https://api.jacktrip.org")); + } + if (!parameters->contains("client_id")) { + parameters->insert("client_id", "cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I"); + } + }); + + m_authenticator->setRefreshToken(m_refreshToken); + m_authenticator->refreshAccessToken(); + } +} + +void VirtualStudio::login() +{ + setupAuthenticator(); + m_authenticator->grant(); +} + +void VirtualStudio::logout() +{ + m_authenticator->setToken(QLatin1String("")); + m_authenticator->setRefreshToken(QLatin1String("")); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.remove(QStringLiteral("RefreshToken")); + settings.remove(QStringLiteral("UserId")); + settings.endGroup(); + + m_refreshTimer.stop(); + + m_refreshToken.clear(); + m_userId.clear(); + emit hasRefreshTokenChanged(); +} + +void VirtualStudio::refreshStudios(int index) +{ + getServerList(false, index); +} + +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)); + + // Make sure we keep our current settings if the device still exists + if (!m_inputDeviceList.contains(m_inputDevice)) { + m_inputDevice = QStringLiteral("(default)"); + } + if (!m_outputDeviceList.contains(m_outputDevice)) { + m_outputDevice = QStringLiteral("(default)"); + } + + emit inputDeviceChanged(); + emit outputDeviceChanged(); +#endif +} + +void VirtualStudio::revertSettings() +{ + m_uiScale = m_previousUiScale; + emit uiScaleChanged(); +#ifdef RT_AUDIO + // Restore our previous settings + m_inputDevice = m_previousInput; + m_outputDevice = m_previousOutput; + m_bufferSize = m_previousBuffer; + m_useRtAudio = m_previousUseRtAudio; + emit inputDeviceChanged(); + emit outputDeviceChanged(); + emit bufferSizeChanged(); + emit audioBackendChanged(); +#endif +} + +void VirtualStudio::applySettings() +{ + m_previousUiScale = m_uiScale; + emit newScale(); + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("UiScale"), m_uiScale); + settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup); + settings.endGroup(); +#ifdef RT_AUDIO + settings.beginGroup(QStringLiteral("Audio")); + settings.setValue(QStringLiteral("Backend"), m_useRtAudio ? 1 : 0); + settings.setValue(QStringLiteral("BufferSize"), m_bufferSize); + settings.setValue(QStringLiteral("InputDevice"), m_inputDevice); + settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice); + settings.endGroup(); + + m_previousUseRtAudio = m_useRtAudio; + m_previousBuffer = m_bufferSize; + m_previousInput = m_inputDevice; + m_previousOutput = m_outputDevice; + + emit inputDeviceChanged(); + emit outputDeviceChanged(); +#endif +} + +void VirtualStudio::connectToStudio(int studioIndex) +{ + { + QMutexLocker locker(&m_refreshMutex); + m_allowRefresh = false; + } + m_refreshTimer.stop(); + + m_currentStudio = studioIndex; + VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); + emit currentStudioChanged(); + m_onConnectedScreen = true; + + // Check if we have an address for our server + if (studioInfo->host().isEmpty()) { + // EXPERIMENTAL CODE. (It shouldn't be possible to arrive here.) + if (studioInfo->isManageable()) { + m_connectionState = QStringLiteral("Starting Studio..."); + emit connectionStateChanged(); + + // Send a put request to start our studio + m_startedStudio = true; + QString expiration = + QDateTime::currentDateTimeUtc().addSecs(60 * 60).toString(Qt::ISODate); + QJsonObject json = {{QLatin1String("enabled"), true}, + {QLatin1String("expiresAt"), expiration}}; + QJsonDocument request = QJsonDocument(json); + + QNetworkReply* reply = m_authenticator->put( + QStringLiteral("https://app.jacktrip.org/api/servers/%1") + .arg(studioInfo->id()), + request.toJson()); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + m_connectionState = QStringLiteral("Unable to Start Studio"); + emit connectionStateChanged(); + } else { + QByteArray response = reply->readAll(); + QJsonDocument serverState = QJsonDocument::fromJson(response); + if (serverState.object()[QStringLiteral("status")].toString() + == QLatin1String("Starting")) { + // Start our timer to check for our hostname + m_startTimer.setInterval(5000); + m_startTimer.start(); + } + } + reply->deleteLater(); + }); + } else { + m_connectionState = QStringLiteral("Unable to Start Studio"); + emit connectionStateChanged(); + m_startedStudio = false; + } + } else { + m_startedStudio = false; + completeConnection(); + } +} + +void VirtualStudio::completeConnection() +{ + if (m_currentStudio < 0) { + return; + } + + m_jackTripRunning = true; + m_connectionState = QStringLiteral("Connecting..."); + emit connectionStateChanged(); + VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); + try { + m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2, +#ifdef WAIR // wair + 0, +#endif // endwhere + 4, 1)); + m_jackTrip->setConnectDefaultAudioPorts(true); +#ifdef RT_AUDIO + if (m_useRtAudio) { + m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); + m_jackTrip->setSampleRate(studioInfo->sampleRate()); + m_jackTrip->setAudioBufferSizeInSamples(m_bufferSize); + if (m_inputDevice == QLatin1String("(default)")) { + m_jackTrip->setInputDevice(""); + } else { + m_jackTrip->setInputDevice(m_inputDevice.toStdString()); + } + if (m_outputDevice == QLatin1String("(default)")) { + m_jackTrip->setOutputDevice(""); + } else { + m_jackTrip->setOutputDevice(m_outputDevice.toStdString()); + } + } +#endif + m_jackTrip->setBufferStrategy(1); + m_jackTrip->setBufferQueueLength(-500); + m_jackTrip->setPeerAddress(studioInfo->host()); + m_jackTrip->setPeerPorts(studioInfo->port()); + m_jackTrip->setPeerHandshakePort(studioInfo->port()); + + QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, + &VirtualStudio::processFinished, Qt::QueuedConnection); + QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this, + &VirtualStudio::processError, Qt::QueuedConnection); + QObject::connect(m_jackTrip.data(), &JackTrip::signalReceivedConnectionFromPeer, + this, &VirtualStudio::receivedConnectionFromPeer, + Qt::QueuedConnection); + + // TODO: replace the following: + // m_ui->statusBar->showMessage(QStringLiteral("Waiting for Peer...")); + /* + QObject::connect(m_jackTrip.data(), &JackTrip::signalUdpWaitingTooLong, this, + &QJackTrip::udpWaitingTooLong, Qt::QueuedConnection); + QObject::connect(m_jackTrip.data(), &JackTrip::signalQueueLengthChanged, this, + &QJackTrip::queueLengthChanged, Qt::QueuedConnection);*/ + +#ifdef WAIRTOHUB // WAIR + m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name +#else + m_jackTrip->startProcess(); +#endif // endwhere + } catch (const std::exception& e) { + // Let the user know what our exception was. + m_connectionState = QStringLiteral("JackTrip Error"); + emit connectionStateChanged(); + + QMessageBox msgBox; + msgBox.setText(QStringLiteral("Error: ").append(e.what())); + msgBox.setWindowTitle(QStringLiteral("Doh!")); + msgBox.exec(); + + m_jackTripRunning = false; + emit disconnected(); + m_onConnectedScreen = false; + return; + } + +#ifdef __APPLE__ + m_noNap.disableNap(); +#endif +} + +void VirtualStudio::disconnect() +{ + m_connectionState = QStringLiteral("Disconnecting..."); + emit connectionStateChanged(); + m_retryPeriodTimer.stop(); + m_retryPeriod = false; + + if (m_jackTripRunning) { + if (m_startedStudio) { + VsServerInfo* studioInfo = + static_cast(m_servers.at(m_currentStudio)); + QMessageBox msgBox; + msgBox.setText(QStringLiteral("Do you want to stop the current studio?")); + msgBox.setWindowTitle(QStringLiteral("Stop Studio")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::Yes); + int ret = msgBox.exec(); + if (ret == QMessageBox::Yes) { + studioInfo->setHost(QLatin1String("")); + stopStudio(); + } + } + m_jackTrip->stop(); + } else if (m_startedStudio) { + m_startTimer.stop(); + stopStudio(); + if (!m_isExiting) { + emit disconnected(); + m_onConnectedScreen = false; + } + } else { + // How did we get here? This shouldn't be possible, but include for safety. + if (m_isExiting) { + emit signalExit(); + } else { + emit disconnected(); + m_onConnectedScreen = false; + } + } + + // Restart our studio refresh timer. + if (!m_isExiting) { + QMutexLocker locker(&m_refreshMutex); + m_allowRefresh = true; + m_refreshTimer.start(); + } +} + +void VirtualStudio::manageStudio(int studioIndex) +{ + if (studioIndex == -1) { + // We're here from a connected screen. Use our current studio. + studioIndex = m_currentStudio; + } + QUrl url = + QUrl(QStringLiteral("https://app.jacktrip.org/studios/%1") + .arg(static_cast(m_servers.at(studioIndex))->id())); + QDesktopServices::openUrl(url); +} + +void VirtualStudio::createStudio() +{ + QUrl url = QUrl(QStringLiteral("https://app.jacktrip.org/studios/create")); + QDesktopServices::openUrl(url); +} + +void VirtualStudio::showAbout() +{ + About about; + about.exec(); +} + +void VirtualStudio::exit() +{ + m_refreshTimer.stop(); + if (m_onConnectedScreen) { + m_isExiting = true; + disconnect(); + } else { + emit signalExit(); + } +} + +void VirtualStudio::slotAuthSucceded() +{ + m_refreshToken = m_authenticator->refreshToken(); + emit hasRefreshTokenChanged(); + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken); + settings.endGroup(); + + settings.setValue(QStringLiteral("UiMode"), QJackTrip::VIRTUAL_STUDIO); + + if (m_userId.isEmpty()) { + getUserId(); + } else { + getSubscriptions(); + } +} + +void VirtualStudio::slotAuthFailed() +{ + emit authFailed(); +} + +void VirtualStudio::processFinished() +{ + if (m_isExiting) { + emit signalExit(); + return; + } + + if (m_retryPeriod && m_startedStudio) { + // Retry if necessary. + completeConnection(); + return; + } + + if (!m_jackTripRunning) { + return; + } + + m_jackTripRunning = false; + m_connectionState = QStringLiteral("Disconnected"); + m_jackTrip.reset(); + emit connectionStateChanged(); + emit disconnected(); + m_onConnectedScreen = false; +#ifdef __APPLE__ + m_noNap.enableNap(); +#endif +} + +void VirtualStudio::processError(const QString& errorMessage) +{ + if (!m_retryPeriod) { + QMessageBox msgBox; + if (errorMessage == QLatin1String("Peer Stopped")) { + // Report the other end quitting as a regular occurance rather than an error. + msgBox.setText(errorMessage); + msgBox.setWindowTitle(QStringLiteral("Disconnected")); + } else { + msgBox.setText(QStringLiteral("Error: ").append(errorMessage)); + msgBox.setWindowTitle(QStringLiteral("Doh!")); + } + msgBox.exec(); + } + processFinished(); +} + +void VirtualStudio::receivedConnectionFromPeer() +{ + m_connectionState = QStringLiteral("Connected"); + emit connectionStateChanged(); + std::cout << "Received connection" << std::endl; + emit connected(); +} + +void VirtualStudio::checkForHostname() +{ + if (m_currentStudio < 0) { + return; + } + + VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); + QNetworkReply* reply = m_authenticator->get( + QStringLiteral("https://app.jacktrip.org/api/servers/%1").arg(studioInfo->id())); + connect(reply, &QNetworkReply::finished, this, [&, reply, studioInfo]() { + if (reply->error() != QNetworkReply::NoError) { + m_connectionState = QStringLiteral("Unable to Start Studio"); + emit connectionStateChanged(); + + // Stop our timer + m_startTimer.stop(); + } else { + QByteArray response = reply->readAll(); + QJsonDocument serverState = QJsonDocument::fromJson(response); + if (serverState.object()[QStringLiteral("status")].toString() + == QLatin1String("Ready")) { + // Ready to connect + m_startTimer.stop(); + studioInfo->setHost( + serverState.object()[QStringLiteral("serverHost")].toString()); + studioInfo->setPort( + serverState.object()[QStringLiteral("serverPort")].toInt()); + m_retryPeriod = true; + m_retryPeriodTimer.setInterval(15000); + m_retryPeriodTimer.start(); + completeConnection(); + } + } + reply->deleteLater(); + ; + }); +} + +void VirtualStudio::endRetryPeriod() +{ + m_retryPeriod = false; + m_retryPeriodTimer.stop(); +} + +void VirtualStudio::launchBrowser(const QUrl& url) +{ + std::cout << "Launching Browser" << std::endl; + bool success = QDesktopServices::openUrl(url); + if (success) { + std::cout << "Success" << std::endl; + } else { + std::cout << "Unable to open URL" << std::endl; + } +} + +void VirtualStudio::setupAuthenticator() +{ + if (m_authenticator.isNull()) { + // Set up our authorization flow + m_authenticator.reset(new QOAuth2AuthorizationCodeFlow); + m_authenticator->setScope( + QStringLiteral("openid profile email offline_access read:servers")); + connect(m_authenticator.data(), + &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, + &VirtualStudio::launchBrowser); + + const QUrl authUri(QStringLiteral("https://auth.jacktrip.org/authorize")); + const QString clientId = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I"); + const QUrl tokenUri(QStringLiteral("https://auth.jacktrip.org/oauth/token")); + const quint16 port = 52424; + + m_authenticator->setAuthorizationUrl(authUri); + m_authenticator->setClientIdentifier(clientId); + m_authenticator->setAccessTokenUrl(tokenUri); + + m_authenticator->setModifyParametersFunction([](QAbstractOAuth2::Stage stage, + QVariantMap* parameters) { + if (stage == QAbstractOAuth2::Stage::RequestingAccessToken) { + QByteArray code = parameters->value(QStringLiteral("code")).toByteArray(); + (*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code); + } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) { + parameters->insert(QStringLiteral("audience"), + QStringLiteral("https://api.jacktrip.org")); + } + }); + + QOAuthHttpServerReplyHandler* replyHandler = + new QOAuthHttpServerReplyHandler(port, this); + replyHandler->setCallbackText(QStringLiteral( + "
\n" + "\n" + "

Virtual " + "Studio Login Successful

\n" + "

You may close this window " + "and return to the JackTrip application.

\n" + "
\n")); + m_authenticator->setReplyHandler(replyHandler); + connect(m_authenticator.data(), &QOAuth2AuthorizationCodeFlow::granted, this, + &VirtualStudio::slotAuthSucceded); + connect(m_authenticator.data(), &QOAuth2AuthorizationCodeFlow::requestFailed, + this, &VirtualStudio::slotAuthFailed); + } +} + +void VirtualStudio::getServerList(bool firstLoad, int index) +{ + { + QMutexLocker locker(&m_refreshMutex); + if (!m_allowRefresh || m_refreshInProgress) { + return; + } else { + m_refreshInProgress = true; + } + } + + // Get the serverId of the server at the top of our screen if we know it + QString topServerId; + if (index >= 0 && index < m_servers.count()) { + topServerId = static_cast(m_servers.at(index))->id(); + } + + QNetworkReply* reply = + m_authenticator->get(QStringLiteral("https://app.jacktrip.org/api/servers")); + connect(reply, &QNetworkReply::finished, this, [&, reply, topServerId, firstLoad]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + + QByteArray response = reply->readAll(); + QJsonDocument serverList = QJsonDocument::fromJson(response); + if (!serverList.isArray()) { + std::cout << "Error: Not an array" << std::endl; + QMutexLocker locker(&m_refreshMutex); + m_refreshInProgress = false; + emit authFailed(); + reply->deleteLater(); + return; + } + QJsonArray servers = serverList.array(); + // Divide our servers by category initially so that they're easier to sort + QList yourServers; + QList subServers; + QList pubServers; + + for (int i = 0; i < servers.count(); i++) { + if (servers.at(i)[QStringLiteral("type")].toString().contains( + QStringLiteral("JackTrip"))) { + VsServerInfo* serverInfo = new VsServerInfo(this); + serverInfo->setIsManageable( + servers.at(i)[QStringLiteral("admin")].toBool()); + QString status = servers.at(i)[QStringLiteral("status")].toString(); + bool activeStudio = status == QLatin1String("Ready"); + bool hostedStudio = servers.at(i)[QStringLiteral("managed")].toBool(); + // Only iterate through servers that we want to show + if (!m_showSelfHosted && !hostedStudio) { + continue; + } + if (!m_showInactive && !activeStudio) { + continue; + } + if (activeStudio || (serverInfo->isManageable() && m_showInactive)) { + serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString()); + serverInfo->setHost( + servers.at(i)[QStringLiteral("serverHost")].toString()); + serverInfo->setStatus( + servers.at(i)[QStringLiteral("status")].toString()); + serverInfo->setPort( + servers.at(i)[QStringLiteral("serverPort")].toInt()); + serverInfo->setIsPublic( + servers.at(i)[QStringLiteral("public")].toBool()); + serverInfo->setRegion( + servers.at(i)[QStringLiteral("region")].toString()); + serverInfo->setPeriod( + servers.at(i)[QStringLiteral("period")].toInt()); + serverInfo->setSampleRate( + servers.at(i)[QStringLiteral("sampleRate")].toInt()); + serverInfo->setQueueBuffer( + servers.at(i)[QStringLiteral("queueBuffer")].toInt()); + serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString()); + if (servers.at(i)[QStringLiteral("owner")].toBool()) { + yourServers.append(serverInfo); + serverInfo->setSection(VsServerInfo::YOUR_STUDIOS); + } else if (m_subscribedServers.contains(serverInfo->id())) { + subServers.append(serverInfo); + serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS); + } else { + pubServers.append(serverInfo); + } + } + } + } + + std::sort(yourServers.begin(), yourServers.end(), + [](QObject* first, QObject* second) { + return static_cast(first)->name() + < static_cast(second)->name(); + }); + std::sort(subServers.begin(), subServers.end(), + [](QObject* first, QObject* second) { + return static_cast(first)->name() + < static_cast(second)->name(); + }); + std::sort(pubServers.begin(), pubServers.end(), + [](QObject* first, QObject* second) { + return static_cast(first)->name() + < static_cast(second)->name(); + }); + + // If we don't have any owned servers, move the JackTrip logo to an appropriate + // section header. + if (yourServers.isEmpty()) { + if (subServers.isEmpty()) { + m_logoSection = QStringLiteral("Public Studios"); + } else { + m_logoSection = QStringLiteral("Subscribed Studios"); + } + emit logoSectionChanged(); + } else { + m_logoSection = QStringLiteral("Your Studios"); + emit logoSectionChanged(); + } + + QMutexLocker locker(&m_refreshMutex); + // Check that we haven't tried connecting to a server between the + // request going out and the response. + if (!m_allowRefresh) { + m_refreshInProgress = false; + return; + } + m_servers.clear(); + m_servers.append(yourServers); + m_servers.append(subServers); + m_servers.append(pubServers); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("serverModel"), QVariant::fromValue(m_servers)); + int index = -1; + if (!topServerId.isEmpty()) { + for (int i = 0; i < m_servers.count(); i++) { + if (static_cast(m_servers.at(i))->id() == topServerId) { + index = i; + break; + } + } + } + if (firstLoad) { + emit authSucceeded(); + m_refreshTimer.setInterval(10000); + m_refreshTimer.start(); + } else { + emit refreshFinished(index); + } + m_refreshInProgress = false; + + reply->deleteLater(); + }); +} + +void VirtualStudio::getUserId() +{ + QNetworkReply* reply = + m_authenticator->get(QStringLiteral("https://auth.jacktrip.org/userinfo")); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + + QByteArray response = reply->readAll(); + QJsonDocument userInfo = QJsonDocument::fromJson(response); + m_userId = userInfo.object()[QStringLiteral("sub")].toString(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("UserId"), m_userId); + settings.endGroup(); + getSubscriptions(); + reply->deleteLater(); + }); +} + +void VirtualStudio::getSubscriptions() +{ + QNetworkReply* reply = m_authenticator->get( + QStringLiteral("https://app.jacktrip.org/api/users/%1/subscriptions") + .arg(m_userId)); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + + QByteArray response = reply->readAll(); + QJsonDocument subscriptionList = QJsonDocument::fromJson(response); + if (!subscriptionList.isArray()) { + std::cout << "Error: Not an array" << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + QJsonArray subscriptions = subscriptionList.array(); + for (int i = 0; i < subscriptions.count(); i++) { + m_subscribedServers.append( + subscriptions.at(i)[QStringLiteral("serverId")].toString()); + } + getServerList(true); + reply->deleteLater(); + }); +} + +#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)); + } + } + } +} +#endif + +void VirtualStudio::stopStudio() +{ + if (m_currentStudio < 0) { + return; + } + + VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); + QJsonObject json = {{QLatin1String("enabled"), false}}; + QJsonDocument request = QJsonDocument(json); + studioInfo->setHost(QLatin1String("")); + QNetworkReply* reply = m_authenticator->put( + QStringLiteral("https://app.jacktrip.org/api/servers/%1").arg(studioInfo->id()), + request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (m_isExiting && !m_jackTripRunning) { + emit signalExit(); + } + reply->deleteLater(); + }); +} + +VirtualStudio::~VirtualStudio() +{ + for (int i = 0; i < m_servers.count(); i++) { + delete m_servers.at(i); + } +} diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h new file mode 100644 index 0000000..d6b2446 --- /dev/null +++ b/src/gui/virtualstudio.h @@ -0,0 +1,253 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file virtualstudio.h + * \author Aaron Wyatt + * \date March 2022 + */ + +#ifndef VIRTUALSTUDIO_H +#define VIRTUALSTUDIO_H + +#include +#include +#include +#include +#include +#include + +#include "../JackTrip.h" +#include "vsQuickView.h" +#include "vsServerInfo.h" + +#ifdef __APPLE__ +#include "NoNap.h" +#endif + +class QJackTrip; + +class VirtualStudio : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool showFirstRun READ showFirstRun NOTIFY showFirstRunChanged) + Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged) + Q_PROPERTY(QString versionString READ versionString CONSTANT) + Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged) + Q_PROPERTY(bool selectableBackend READ selectableBackend CONSTANT) + Q_PROPERTY(QString audioBackend READ audioBackend WRITE setAudioBackend NOTIFY + audioBackendChanged) + Q_PROPERTY( + int inputDevice READ inputDevice WRITE setInputDevice NOTIFY inputDeviceChanged) + Q_PROPERTY(int outputDevice READ outputDevice WRITE setOutputDevice NOTIFY + outputDeviceChanged) + Q_PROPERTY( + int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) + Q_PROPERTY(int currentStudio READ currentStudio NOTIFY currentStudioChanged) + Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY + showInactiveChanged) + Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY + showSelfHostedChanged) + Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged) + Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY + updateChannelChanged) + Q_PROPERTY(float fontScale READ fontScale CONSTANT) + Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged) + Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged) + Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY + showDeviceSetupChanged) + Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY + showWarningsChanged) + Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT) + Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT) + + public: + explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr); + ~VirtualStudio() override; + + void setStandardWindow(QSharedPointer window); + void show(); + + bool showFirstRun(); + bool hasRefreshToken(); + QString versionString(); + QString logoSection(); + bool selectableBackend(); + QString audioBackend(); + void setAudioBackend(const QString& backend); + int inputDevice(); + void setInputDevice(int device); + int outputDevice(); + void setOutputDevice(int device); + int bufferSize(); + void setBufferSize(int index); + int currentStudio(); + QString connectionState(); + QString updateChannel(); + void setUpdateChannel(const QString& channel); + bool showInactive(); + void setShowInactive(bool inactive); + bool showSelfHosted(); + void setShowSelfHosted(bool selfHosted); + float fontScale(); + float uiScale(); + void setUiScale(float scale); + bool darkMode(); + void setDarkMode(bool dark); + bool showDeviceSetup(); + void setShowDeviceSetup(bool show); + bool showWarnings(); + void setShowWarnings(bool show); + bool noUpdater(); + bool psiBuild(); + + public slots: + void toStandard(); + void toVirtualStudio(); + void login(); + void logout(); + void refreshStudios(int index); + void refreshDevices(); + void revertSettings(); + void applySettings(); + void connectToStudio(int studioIndex); + void completeConnection(); + void disconnect(); + void manageStudio(int studioIndex); + void createStudio(); + void showAbout(); + void exit(); + + signals: + void authSucceeded(); + void authFailed(); + void connected(); + void disconnected(); + void refreshFinished(int index); + void showFirstRunChanged(); + void hasRefreshTokenChanged(); + void logoSectionChanged(); + void audioBackendChanged(); + void inputDeviceChanged(); + void outputDeviceChanged(); + void bufferSizeChanged(); + void currentStudioChanged(); + void showInactiveChanged(); + void showSelfHostedChanged(); + void connectionStateChanged(); + void updateChannelChanged(); + void showDeviceSetupChanged(); + void showWarningsChanged(); + void uiScaleChanged(); + void newScale(); + void darkModeChanged(); + void signalExit(); + void periodicRefresh(); + + private slots: + void slotAuthSucceded(); + void slotAuthFailed(); + void processFinished(); + void processError(const QString& errorMessage); + void receivedConnectionFromPeer(); + void checkForHostname(); + void endRetryPeriod(); + void launchBrowser(const QUrl& url); + + private: + void setupAuthenticator(); + void getServerList(bool firstLoad = false, int index = -1); + void getUserId(); + void getSubscriptions(); +#ifdef RT_AUDIO + void getDeviceList(QStringList* list, bool isInput); +#endif + void stopStudio(); + + bool m_showFirstRun = false; + bool m_checkSsl = true; + QString m_updateChannel; + QString m_refreshToken; + QString m_userId; + VsQuickView m_view; + QSharedPointer m_standardWindow; + QScopedPointer m_authenticator; + + QList m_servers; + QStringList m_subscribedServers; + QString m_logoSection = QStringLiteral("Your Studios"); + bool m_selectableBackend = true; + bool m_useRtAudio = false; + int m_currentStudio = -1; + QString m_connectionState = QStringLiteral("Connecting..."); + QScopedPointer m_jackTrip; + QTimer m_startTimer; + QTimer m_retryPeriodTimer; + bool m_startedStudio = false; + bool m_retryPeriod; + bool m_jackTripRunning = false; + + QTimer m_refreshTimer; + QMutex m_refreshMutex; + bool m_allowRefresh = true; + bool m_refreshInProgress = false; + + bool m_onConnectedScreen = false; + bool m_isExiting = false; + bool m_showInactive = false; + bool m_showSelfHosted = false; + bool m_showDeviceSetup = true; + bool m_showWarnings = true; + float m_fontScale = 1; + float m_uiScale; + float m_previousUiScale; + bool m_darkMode = false; + +#ifdef RT_AUDIO + QStringList m_inputDeviceList; + QStringList m_outputDeviceList; + QString m_inputDevice; + QString m_outputDevice; + quint16 m_bufferSize; + QString m_previousInput; + QString m_previousOutput; + quint16 m_previousBuffer; + bool m_previousUseRtAudio = false; +#endif + QStringList m_bufferOptions = {"16", "32", "64", "128", "256", "512", "1024"}; + QStringList m_updateChannelOptions = {"Stable", "Edge"}; + +#ifdef __APPLE__ + NoNap m_noNap; +#endif +}; + +#endif // VIRTUALSTUDIO_H diff --git a/src/gui/vs.qml b/src/gui/vs.qml new file mode 100644 index 0000000..1465189 --- /dev/null +++ b/src/gui/vs.qml @@ -0,0 +1,122 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Rectangle { + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + + width: 696 + height: 577 + color: backgroundColour + state: virtualstudio.showFirstRun ? "start" : "login" + anchors.fill: parent + + id: window + states: [ + State { + name: "start" + PropertyChanges { target: startScreen; x: 0 } + PropertyChanges { target: loginScreen; x: window.width; failTextVisible: loginScreen.failTextVisible } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + }, + + State { + name: "login" + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: loginScreen; x: 0; failTextVisible: false } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + }, + + State { + name: "setup" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: setupScreen; x: 0 } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + }, + + State { + name: "browse" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: setupScreen; x: -setupScreen.width } + PropertyChanges { target: browseScreen; x: 0 } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + }, + + State { + name: "settings" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: setupScreen; x: -setupScreen.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: 0 } + PropertyChanges { target: connectedScreen; x: window.width } + }, + + State { + name: "connected" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: setupScreen; x: -setupScreen.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: 0 } + } + ] + + transitions: Transition { + NumberAnimation { properties: "x"; duration: 800; easing.type: Easing.InOutQuad } + } + + FirstLaunch { + id: startScreen + } + + Setup { + id: setupScreen + } + + Browse { + id: browseScreen + } + + Login { + id: loginScreen + } + + Settings { + id: settingsScreen + } + + Connected { + id: connectedScreen + } + + Connections { + target: virtualstudio + onAuthSucceeded: { + if (virtualstudio.showDeviceSetup) { + window.state = "setup"; + } else { + window.state = "browse"; + } + } + onAuthFailed: { + loginScreen.failTextVisible = true; + } + // onConnected: { } + onDisconnected: { + window.state = "browse"; + } + } +} diff --git a/src/gui/vsQuickView.cpp b/src/gui/vsQuickView.cpp new file mode 100644 index 0000000..791104f --- /dev/null +++ b/src/gui/vsQuickView.cpp @@ -0,0 +1,47 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsQuickView.cpp + * \author Aaron Wyatt + * \date March 2022 + */ + +#include "vsQuickView.h" + +bool VsQuickView::event(QEvent* event) +{ + if (event->type() == QEvent::Close) { + emit windowClose(); + event->ignore(); + } + return QQuickView::event(event); +} diff --git a/src/gui/vsQuickView.h b/src/gui/vsQuickView.h new file mode 100644 index 0000000..bb9ad78 --- /dev/null +++ b/src/gui/vsQuickView.h @@ -0,0 +1,55 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsQuickView.h + * \author Aaron Wyatt + * \date March 2022 + */ + +#ifndef VSQUICKVIEW_H +#define VSQUICKVIEW_H + +#include + +class VsQuickView : public QQuickView +{ + Q_OBJECT + + public: + VsQuickView(QWindow* parent = nullptr) : QQuickView(parent) {} + bool event(QEvent* event) override; + + signals: + void windowClose(); +}; + +#endif // VSQUICKVIEW_H diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp new file mode 100644 index 0000000..ec9a761 --- /dev/null +++ b/src/gui/vsServerInfo.cpp @@ -0,0 +1,211 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsServerInfo.cpp + * \author Aaron Wyatt + * \date March 2022 + */ + +#include "vsServerInfo.h" + +VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {} + +VsServerInfo::serverSectionT VsServerInfo::section() +{ + return m_section; +} + +QString VsServerInfo::type() +{ + if (m_section == YOUR_STUDIOS) { + return QStringLiteral("Your Studios"); + } else if (m_section == SUBSCRIBED_STUDIOS) { + return QStringLiteral("Subscribed Studios"); + } else { + return QStringLiteral("Public Studios"); + } +} + +void VsServerInfo::setSection(serverSectionT section) +{ + m_section = section; +} + +QString VsServerInfo::name() +{ + return m_name; +} + +void VsServerInfo::setName(const QString& name) +{ + m_name = name; +} + +QString VsServerInfo::host() +{ + return m_host; +} + +QString VsServerInfo::status() +{ + return m_status; +} + +bool VsServerInfo::canConnect() +{ + return !m_host.isEmpty() && m_status == "Ready"; +} + +bool VsServerInfo::canStart() +{ +#ifdef PSI + return true; +#else + return false; +#endif +} + +void VsServerInfo::setHost(const QString& host) +{ + m_host = host; + emit canConnectChanged(); +} + +void VsServerInfo::setStatus(const QString& status) +{ + m_status = status; + emit canConnectChanged(); +} + +quint16 VsServerInfo::port() +{ + return m_port; +} + +void VsServerInfo::setPort(quint16 port) +{ + m_port = port; +} + +bool VsServerInfo::isPublic() +{ + return m_isPublic; +} + +void VsServerInfo::setIsPublic(bool isPublic) +{ + m_isPublic = isPublic; +} + +QString VsServerInfo::region() +{ + return m_region; +} + +QString VsServerInfo::flag() +{ + QStringList parts = m_region.split(QStringLiteral("-")); + if (parts.count() > 1) { + QString countryCode = parts.at(1).toUpper(); + if (countryCode == QStringLiteral("TF")) { + countryCode = QStringLiteral("TW"); + } + return QStringLiteral("flags/%1.svg").arg(countryCode); + } + // Have a fallback here + return QStringLiteral("flags/US.svg"); +} + +QString VsServerInfo::location() +{ + if (m_region.split(QStringLiteral("-")).count() > 2) { + return m_region.section(QStringLiteral("-"), 2); + } + return m_region; +} + +void VsServerInfo::setRegion(const QString& region) +{ + m_region = region; +} + +bool VsServerInfo::isManageable() +{ + return m_isManageable; +} + +void VsServerInfo::setIsManageable(bool isManageable) +{ + m_isManageable = isManageable; +} + +quint16 VsServerInfo::period() +{ + return m_period; +} + +void VsServerInfo::setPeriod(quint16 period) +{ + m_period = period; +} + +quint32 VsServerInfo::sampleRate() +{ + return m_sampleRate; +} + +void VsServerInfo::setSampleRate(quint32 sampleRate) +{ + m_sampleRate = sampleRate; +} + +quint16 VsServerInfo::queueBuffer() +{ + return m_queueBuffer; +} + +void VsServerInfo::setQueueBuffer(quint16 queueBuffer) +{ + m_queueBuffer = queueBuffer; +} + +QString VsServerInfo::id() +{ + return m_id; +} + +void VsServerInfo::setId(const QString& id) +{ + m_id = id; +} + +VsServerInfo::~VsServerInfo() = default; diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h new file mode 100644 index 0000000..fecb849 --- /dev/null +++ b/src/gui/vsServerInfo.h @@ -0,0 +1,137 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsServerInfo.h + * \author Aaron Wyatt + * \date March 2022 + */ + +#ifndef VSSERVERINFO_H +#define VSSERVERINFO_H + +#include + +class VsServerInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString type READ type CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + // Q_PROPERTY(QString host READ host CONSTANT) + Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged) + Q_PROPERTY(bool canStart READ canStart CONSTANT) + // Q_PROPERTY(quint16 port READ port CONSTANT) + Q_PROPERTY(bool isPublic READ isPublic CONSTANT) + Q_PROPERTY(QString flag READ flag CONSTANT) + Q_PROPERTY(QString location READ location CONSTANT) + Q_PROPERTY(bool isManageable READ isManageable CONSTANT) + Q_PROPERTY(quint16 period READ period CONSTANT) + Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT) + Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT) + Q_PROPERTY(QString status READ status CONSTANT) + + public: + enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS }; + + explicit VsServerInfo(QObject* parent = nullptr); + ~VsServerInfo() override; + + serverSectionT section(); + QString type(); + void setSection(serverSectionT section); + QString name(); + void setName(const QString& name); + QString host(); + bool canConnect(); + bool canStart(); + void setHost(const QString& host); + quint16 port(); + void setPort(quint16 port); + bool isPublic(); + void setIsPublic(bool isPublic); + QString region(); + QString flag(); + QString location(); + void setRegion(const QString& region); + bool isManageable(); + void setIsManageable(bool isManageable); + quint16 period(); + void setPeriod(quint16 period); + quint32 sampleRate(); + void setSampleRate(quint32 sampleRate); + quint16 queueBuffer(); + void setQueueBuffer(quint16 queueBuffer); + QString id(); + void setId(const QString& id); + QString status(); + void setStatus(const QString& status); + + signals: + void canConnectChanged(); + + private: + serverSectionT m_section = PUBLIC_STUDIOS; + QString m_name; + QString m_host; + quint16 m_port; + bool m_isPublic; + QString m_region; + bool m_isManageable; + quint16 m_period; + quint32 m_sampleRate; + quint16 m_queueBuffer; + QString m_id; + QString m_status; + + /* Remaining JSON fields + "loopback": true, + "stereo": true, + "type": "JackTrip", + "managed": true, + "size": "c5.large", + "mixBranch": "main", + "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;", + "enabled": true, + "admin": true, + "cloudId": "string", + "owner": true, + "ownerId": "string", + "status": "Ready", + "sessionId": "1636042722abcdefg", + "subStatus": "Active", + "createdAt": "2021-09-07T17:15:38Z", + "expiresAt": "2021-09-07T17:15:38Z", + "updatedAt": "2021-09-07T17:15:38Z" + */ +}; + +#endif // VSSERVERINFO_H diff --git a/src/gui/wedge.svg b/src/gui/wedge.svg new file mode 100644 index 0000000..230cdd2 --- /dev/null +++ b/src/gui/wedge.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/src/gui/wedge_inactive.svg b/src/gui/wedge_inactive.svg new file mode 100644 index 0000000..68ecbcf --- /dev/null +++ b/src/gui/wedge_inactive.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index b71be81..c20c196 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "1.5.3"; ///< JackTrip version +constexpr const char* const gVersion = "1.6.1"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/main.cpp b/src/main.cpp index 6ff7909..ebe553b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,6 +39,18 @@ #include #include +#ifndef NO_UPDATER +#include "dblsqd/feed.h" +#include "dblsqd/update_dialog.h" +#endif + +#ifndef NO_VS +#include +#include + +#include "gui/virtualstudio.h" +#endif + #include "gui/qjacktrip.h" #else #include @@ -109,6 +121,8 @@ QCoreApplication* createApplication(int& argc, char* argv[]) std::exit(1); } #endif + // Turn on high DPI support. + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); return new QApplication(argc, argv); #endif // NO_GUI } else { @@ -199,6 +213,9 @@ bool isRunFromCmd() if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) { return true; } + if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) { + return true; + } } else { CloseHandle(h); } @@ -214,7 +231,12 @@ int main(int argc, char* argv[]) QScopedPointer jackTrip; QScopedPointer udpHub; #ifndef NO_GUI - QScopedPointer window; + QSharedPointer window; + +#ifndef NO_VS + QSharedPointer vs; +#endif + if (qobject_cast(app.data())) { // Start the GUI if there are no command line options. #ifdef _WIN32 @@ -224,7 +246,10 @@ int main(int argc, char* argv[]) FreeConsole(); } #endif // _WIN32 - app->setApplicationName(QStringLiteral("QJackTrip")); + app->setOrganizationName(QStringLiteral("jacktrip")); + app->setOrganizationDomain(QStringLiteral("jacktrip.org")); + app->setApplicationName(QStringLiteral("JackTrip")); + app->setApplicationVersion(gVersion); QCommandLineParser parser; QCommandLineOption verboseOption(QStringList() << QStringLiteral("V") @@ -235,10 +260,55 @@ int main(int argc, char* argv[]) gVerboseFlag = true; } +#ifndef NO_VS + // Check if we need to show our first run window. + QSettings settings; + int uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); + QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable") + .toString() + .toLower(); +#endif // NO_VS window.reset(new QJackTrip(argc)); QObject::connect(window.data(), &QJackTrip::signalExit, app.data(), &QCoreApplication::quit, Qt::QueuedConnection); +#ifndef NO_VS + vs.reset(new VirtualStudio(uiMode == QJackTrip::UNSET)); + QObject::connect(vs.data(), &VirtualStudio::signalExit, app.data(), + &QCoreApplication::quit, Qt::QueuedConnection); + vs->setStandardWindow(window); + window->setVs(vs); + + if (uiMode == QJackTrip::UNSET) { + vs->show(); + } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) { + vs->show(); + } else { + window->show(); + } +#else window->show(); +#endif // NO_VS + +#ifndef NO_UPDATER + // Setup auto-update feed + dblsqd::Feed* feed = 0; + QString baseUrl = + "https://raw.githubusercontent.com/jacktrip/jacktrip/dev/releases"; +#ifdef Q_OS_WIN + feed = new dblsqd::Feed(); + feed->setUrl( + QUrl(QString("%1/%2/%3-manifests.json").arg(baseUrl, updateChannel, "win"))); +#endif +#ifdef Q_OS_MACOS + feed = new dblsqd::Feed(); + feed->setUrl( + QUrl(QString("%1/%2/%3-manifests.json").arg(baseUrl, updateChannel, "mac"))); +#endif + if (feed) { + dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed); + updateDialog->setIcon(":/qjacktrip/icon.png"); + } +#endif // NO_UPDATER } else { #endif // NO_GUI // Otherwise use the non-GUI version, and parse our command line. diff --git a/win/CodeSignTool/CodeSignTool.sh b/win/CodeSignTool/CodeSignTool.sh new file mode 100755 index 0000000..10c4bc9 --- /dev/null +++ b/win/CodeSignTool/CodeSignTool.sh @@ -0,0 +1 @@ +java -cp "./jar/picocli-4.6.1.jar:./jar/bcprov-jdk15on-1.65.01.jar:./jar/httpclient-4.5.13.jar:./jar/json-simple-1.1.1.jar:./jar/jsign-core-3.1.jar:./jar/commons-io-2.8.0.jar:./jar/bcpkix-jdk15on-1.65.jar:./jar/code_sign_tool-1.2.2.jar:./jar/httpcore-4.4.13.jar:./jar/commons-logging-1.2.jar:./jar/log4j-api-2.17.1.jar:./jar/log4j-core-2.17.1.jar:./jar/poi-4.1.2.jar:./jar/commons-lang3-3.9.jar:./jar/commons-math3-3.6.1.jar:./jar/totp-1.0.jar:./jar/commons-codec-1.15.jar" com.ssl.code.signing.tool.CodeSignTool $@ diff --git a/win/CodeSignTool/conf/code_sign_tool.properties b/win/CodeSignTool/conf/code_sign_tool.properties new file mode 100644 index 0000000..5622896 --- /dev/null +++ b/win/CodeSignTool/conf/code_sign_tool.properties @@ -0,0 +1,4 @@ +CLIENT_ID=kaXTRACNijSWsFdRKg_KAfD3fqrBlzMbWs6TwWHwAn8 +OAUTH2_ENDPOINT=https://login.ssl.com/oauth2/token +CSC_API_ENDPOINT=https://cs.ssl.com +TSA_URL=http://ts.ssl.com \ No newline at end of file diff --git a/win/CodeSignTool/conf/log4j2.xml b/win/CodeSignTool/conf/log4j2.xml new file mode 100644 index 0000000..e3ee419 --- /dev/null +++ b/win/CodeSignTool/conf/log4j2.xml @@ -0,0 +1,28 @@ + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + ${LOG_PATTERN} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/win/build_installer.bat b/win/build_installer.bat index 80f5a14..71a5340 100755 --- a/win/build_installer.bat +++ b/win/build_installer.bat @@ -44,13 +44,19 @@ set "WIXDEFINES=" for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr Qt5Core.dll') do set DYNAMIC_QT=%%a if defined DYNAMIC_QT ( echo Including Qt Files - %QTBINPATH%\windeployqt jacktrip.exe + for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr Qt5Qml.dll') do set VS=%%a + if defined VS ( + %QTBINPATH%\windeployqt --qmldir ..\..\src\gui jacktrip.exe + set WIXDEFINES=%WIXDEFINES% -dvs + ) else ( + %QTBINPATH%\windeployqt jacktrip.exe + ) copy "%QTLIBPATH%\libgcc_s_seh-1.dll" .\ copy "%QTLIBPATH%\libstdc++-6.dll" .\ copy "%QTLIBPATH%\libwinpthread-1.dll" .\ copy "%SSLPATH%\libcrypto-1_1-x64.dll" .\ copy "%SSLPATH%\libssl-1_1-x64.dll" .\ - set WIXDEFINES=%WIXDEFINES% -ddynamic + set WIXDEFINES=!WIXDEFINES! -ddynamic ) for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr librtaudio.dll') do set RTAUDIO=%%a if defined RTAUDIO ( @@ -70,6 +76,6 @@ rem Get our version number for /f "tokens=*" %%a in ('.\jacktrip -v ^| findstr VERSION') do for %%b in (%%~a) do set VERSION=%%b for /f "tokens=1 delims=-" %%a in ("%VERSION%") do set VERSION=%%a echo Version=%VERSION% -candle.exe -dVersion=%VERSION%%WIXDEFINES% jacktrip.wxs files.wxs -light.exe -ext WixUIExtension -o JackTrip.msi jacktrip.wixobj files.wixobj +candle.exe -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% jacktrip.wxs files.wxs +light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj files.wixobj endlocal diff --git a/win/files.wxs b/win/files.wxs index 94c3a3c..00e2c1e 100644 --- a/win/files.wxs +++ b/win/files.wxs @@ -179,6 +179,9 @@ + + + @@ -186,6 +189,1161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -247,8 +1405,384 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/win/jacktrip.wxs b/win/jacktrip.wxs index f34dab9..d6901b8 100644 --- a/win/jacktrip.wxs +++ b/win/jacktrip.wxs @@ -34,10 +34,20 @@ + + + + - + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed +