From: IOhannes m zmölnig (Debian/GNU) Date: Fri, 14 Apr 2023 10:38:09 +0000 (+0200) Subject: New upstream version 1.8.1+ds X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1~1^2~9^2~12 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=c1c7af321c8b50bc42d4f66ed6ab4527337e4771;p=jacktrip.git New upstream version 1.8.1+ds --- diff --git a/.clang-format b/.clang-format index 9b34801..273d78a 100644 --- a/.clang-format +++ b/.clang-format @@ -178,5 +178,7 @@ WhitespaceSensitiveMacros: - STRINGIZE - PP_STRINGIZE - BOOST_PP_STRINGIZE +--- +Language: ObjC +DisableFormat: true ... - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bcffe68 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +fail_fast: false +repos: + - repo: https://github.com/ssciwr/clang-format-hook + rev: v13.0.1 + hooks: + - id: clang-format + files: ^src/ + types_or: [c++] + exclude: '^src/.+dsp\.h' \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ea77b8..d96731b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,7 @@ if (rtaudio) add_compile_definitions(RT_AUDIO) set (qjacktrip_SRC ${qjacktrip_SRC} src/RtAudioInterface.cpp + src/StereoToMono.cpp ) endif () @@ -179,6 +180,7 @@ if (NOT nogui) if (NOT novs) set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/virtualstudio.cpp + src/gui/vsInit.cpp src/gui/vsQuickView.cpp src/gui/vsServerInfo.cpp src/gui/vsPing.cpp diff --git a/build-aux/flatpak/org.jacktrip.JackTrip.json b/build-aux/flatpak/org.jacktrip.JackTrip.json new file mode 100644 index 0000000..64c2f1c --- /dev/null +++ b/build-aux/flatpak/org.jacktrip.JackTrip.json @@ -0,0 +1,36 @@ +{ + "app-id": "org.jacktrip.JackTrip", + "runtime": "org.kde.Platform", + "runtime-version": "5.15-22.08", + "sdk": "org.kde.Sdk", + "command": "jacktrip", + "finish-args": [ + "--share=ipc", + "--socket=x11", + "--device=dri", + "--share=network", + "--filesystem=xdg-run/pipewire-0", + "--env=PIPEWIRE_LATENCY=256/48000" + ], + "cleanup": [ + "/lib/python3.10", + "/share/man" + ], + "modules": [ + "pypi-dependencies.json", + { + "name": "jacktrip", + "buildsystem": "meson", + "config-opts": [ + "-Dbuildtype=debugoptimized" + ], + "sources": [ + { + "type": "git", + "disable-submodules": true, + "url": "https://github.com/jacktrip/jacktrip/" + } + ] + } + ] +} \ No newline at end of file diff --git a/build-aux/flatpak/pypi-dependencies.json b/build-aux/flatpak/pypi-dependencies.json new file mode 100644 index 0000000..e148c9d --- /dev/null +++ b/build-aux/flatpak/pypi-dependencies.json @@ -0,0 +1,40 @@ +{ + "name": "pypi-dependencies", + "buildsystem": "simple", + "build-commands": [], + "modules": [ + { + "name": "python3-pyyaml", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pyyaml\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz", + "sha256": "68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2" + } + ] + }, + { + "name": "python3-jinja2", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"jinja2\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", + "sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/95/7e/68018b70268fb4a2a605e2be44ab7b4dd7ce7808adae6c5ef32e34f4b55a/MarkupSafe-2.1.2.tar.gz", + "sha256": "abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d" + } + ] + } + ] +} \ No newline at end of file diff --git a/build-aux/flatpak/requirements.txt b/build-aux/flatpak/requirements.txt new file mode 100644 index 0000000..33afd92 --- /dev/null +++ b/build-aux/flatpak/requirements.txt @@ -0,0 +1,2 @@ +pyyaml +jinja2 diff --git a/build-aux/flatpak/update_pip_deps.sh b/build-aux/flatpak/update_pip_deps.sh new file mode 100755 index 0000000..b67a299 --- /dev/null +++ b/build-aux/flatpak/update_pip_deps.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# https://github.com/flatpak/flatpak-builder-tools/tree/master/pip +flatpak-pip-generator --runtime='org.freedesktop.Sdk//22.08' --requirements-file='requirements.txt' --output pypi-dependencies \ No newline at end of file diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 3b2df22..c8d3af8 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -19,8 +19,7 @@ Optional: dnf install qt5-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel dnf groupinstall "C Development Tools and Libraries" dnf groupinstall "Development Tools" -dnf install "pkgconfig(jack)" alsa-lib-devel git help2man -dnf install qjackctl +dnf install "pkgconfig(jack)" rtaudio-devel git help2man ``` Clone the git repo with submodules and run `./build install` in the project @@ -28,8 +27,8 @@ directory or use QtCreator to compile. ### Ubuntu and Debian/Raspbian ```sh -apt install --no-install-recommends build-essential qt5-default autoconf automake libtool make libjack-jackd2-dev git help2man -apt install qjackctl qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev libqt5websockets5-dev qtdeclarative5-dev qml-module-qtquick-controls +apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man +apt install qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev libqt5websockets5-dev qtdeclarative5-dev qtquickcontrols2-5-dev apt install librtaudio-dev # if building with RtAudio ``` diff --git a/docs/Build/Meson_build.md b/docs/Build/Meson_build.md index b416f7d..6b95d69 100644 --- a/docs/Build/Meson_build.md +++ b/docs/Build/Meson_build.md @@ -14,7 +14,7 @@ find its documentation at [mesonbuild.com](https://mesonbuild.com/). === "Debian/Ubuntu" ```bash - apt install meson build-essential qtbase5-dev librtaudio-dev libjack-jackd2-dev help2man + apt install meson build-essential qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev libqt5websockets5-dev qtdeclarative5-dev qtquickcontrols2-5-dev libjack-jackd2-dev git help2man ``` === "MacOS" diff --git a/docs/DevTools/Formatting.md b/docs/DevTools/Formatting.md index 17f88c1..6abc7b3 100644 --- a/docs/DevTools/Formatting.md +++ b/docs/DevTools/Formatting.md @@ -23,12 +23,54 @@ this+= isnot ; this = again; ``` -## Formatting the entire code base +## Forget about formatting with a git pre-commit hook -Formatting the entire code base can be done with specifying all code files of the project -or be invoking Meson's clang-format target: +With git the user can install hooks that are executed when specific tasks are done. +We can add a pre-commit hook for clang-format. So that everytime we commit our +changes, git runs clang-format for us. + +Handling git hooks by hand is cumbersome. With [pre-commit](https://pre-commit.com/) +this becomes a lot easier. + +Pre-commit is a Python app that can be installed with pip. ```bash -ninja -C builddir clang-format +pip install pre-commit ``` +Within the root directory of the jacktrip repository the pre-commit hook can be +installed as follows: + +```bash +pre-commit install +``` + +Pre-commit only runs on changed files. Running pre-commit on all files is done +by: + +```bash +pre-commit run --all-files +``` + +Sometimes these hooks come into your way. But you can disable them when committing: + +```bash +git commit -am "Commit all my stuff" --no-verify +``` + +### Pre-commit configuration + +Pre-commit is configured by the *.pre-commit-config.yaml* file in the repository's +root. Currently it only includes the clang-format hook. + +```yaml +fail_fast: false +repos: + - repo: https://github.com/ssciwr/clang-format-hook + rev: v13.0.1 + hooks: + - id: clang-format + files: ^src/ + types_or: [c++] + exclude: '^src/.+dsp\.h' +``` diff --git a/docs/Documentation/MkDocs.md b/docs/Documentation/MkDocs.md index 9339448..ab50a26 100644 --- a/docs/Documentation/MkDocs.md +++ b/docs/Documentation/MkDocs.md @@ -1,17 +1,22 @@ # Write Documentation -This documentation of JackTrip is generated with [Material](https://squidfunk.github.io/mkdocs-material/) +This documentation of JackTrip is generated with [Material](https://squidfunk.github.io/mkdocs-material/) theme for [MkDocs](https://www.mkdocs.org/). All pages are derived from Markdown files in the subdirectory `docs` in JackTrip's git repository. Setup and table of contents is found in a YAML file called mkdocs.yml in the root directory. If you only want to edit a page you can click on the pen symbol at the top of each page. +## MkDocs preview in PRs + +If you submit changes to the docs as a Pull Request, the `Render docs preview` workflow will generate a static version of the documentation, including the proposed changes. It can be downloaded from GitHub Actions for checking offline (see "Checks" tab on top of the PRs page, then select `Render docs preview` and find the archive at the bottom of that page). + +When you're working on the changes locally, it might be more convenient to test changes as they're being made by running mkdocs on your system (see below). ## Run MkDocs on Your System MkDocs and Material for MkDocs are installed from pip: ```bash -pip install mkdocs mkdocs-material +pip install mkdocs mkdocs-material mkdocs-macros-plugin ``` When writing documentation it is very handy to run `mkdocs serve`. This will open diff --git a/docs/Install.md b/docs/Install.md index f44fa5c..14ef46d 100644 --- a/docs/Install.md +++ b/docs/Install.md @@ -14,7 +14,39 @@ On Linux the easiest way to install JackTrip is to use the distribution's packag sudo apt install jacktrip ``` -The [GitHub releases page](https://github.com/jacktrip/jacktrip/releases) also includes a binary which should run on most Linux distributions (x64). This build is known to not look well under Wayland. +### Latest release + +If your distribution doesn't include the latest release in their repository, there are different +alternative options to install from. + +=== "Ubuntu" + + Ubuntu users can install from a [PPA repository](https://launchpad.net/~umlaeute/+archive/ubuntu/jacktrip/) + maintained by the packager of Ubuntu's official JackTrip packages. + + ```bash + sudo add-apt-repository ppa:umlaeute/jacktrip + sudo apt update + sudo apt install jacktrip + ``` + +=== "Debian" + + Debian stable users can obtain their latest version from the [Debian backports](https://backports.debian.org/) repository. + + As an example for Debian Bullseye follow these steps: + + ```bash + mkdir -p /etc/apt/sources.list.d/ + echo "deb http://deb.debian.org/debian bullseye-backports main" | tee /etc/apt/sources.list.d/backports.list + apt-get update + apt-get install -t bullseye-backports jacktrip + ``` + +=== "All" + + The [GitHub releases page](https://github.com/jacktrip/jacktrip/releases) also includes a binary + which should run on most Linux distributions (x64). This build is known to not look well under Wayland. ## macOS macOS installer and application bundle are available on the [GitHub releases page](https://github.com/jacktrip/jacktrip/releases). The installer will install the JackTrip app in `/Applications`, as well as create a link to the `jacktrip` executable in `/usr/local/bin` for use in the command line. diff --git a/docs/assets/jacktrip.svg b/docs/assets/jacktrip.svg new file mode 100644 index 0000000..2d1d67f --- /dev/null +++ b/docs/assets/jacktrip.svg @@ -0,0 +1,10 @@ + + + jacktrip + + + + + + + \ No newline at end of file diff --git a/docs/changelog.yml b/docs/changelog.yml index dd9bc72..8db1bd5 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,56 @@ +- Version: "1.8.1" + Date: 2023-03-29 + Description: + - (added) VS mode - tooltips to explain input and output device + - (added) buffer size and sample rate now settable with Pipewire + - (added) VS mode - link to create a studio on the login complete page + - (fixed) Device names with special characters work again + - (fixed) undefine boolean error + - (fixed) VS Mode - Audio settings correctly show the selected device + - (fixed) VS Mode - Refresh button doesn't crash Windows any more + - (fixed) VS Mode - Output channel selections save correctly + - (updated) documentation styling + - (updated) removed ipify from VS mode + - (updated) ip check can now function with only IPv6 + - (updated) cleaned up vsinit class +- Version: "1.8.0" + Date: 2023-03-01 + Description: + - (added) Qt version option for Meson builds + - (added) GHA builds now include static preview docs + - (added) when using the classic GUI, command line options are now parsed + - (added) VS mode - Selecting and configuring device channels + - (added) Classic mode - Warning for machines without JACK installed + - (added) VS mode - high latency warning for non-ASIO devices + - (added) Meson build without rtaudio in GHA + - (updated) icons in VS mode + - (updated) Linux builds now use Qt 5.15.8 + - (updated) Replaced QVector in meter code + - (updated) Removed set-output from GHA scripts for deprecation + - (updated) Automated the auto-updater release process + - (updated) RtAudio is included in Linux binary releases + - (updated) text on audio setup confirm button when using deeplink + - (updated) VS Mode - improved first-time signin + - (updated) Flathub - improve latency by defaulting to 256 samples buffer size + - (fixed) ambiguous call to overloaded function in Qt6 + - (fixed) issue where selected devices were not the devices used for output + - (fixed) crash when using Classic mode and CLI w/ JACK + - (fixed) ipify issue with Norton + - (fixed) compiler warnings when building without RtAudio + - (fixed) VS Mode - refresh button behavior on settings page +- Version: "1.7.1" + Date: 2023-02-03 + Description: + - (added) missing QuickControl2 dependency + - (added) documentation preview in build steps + - (added) README text about PPA and Debian backports + - (updated) upgraded to Qt 5.15.3 + - (updated) linux package dependencies + - (updated) JackTrip now uses a random available port when connecting + - (updated) VS mode - Audio settings screen layouts + - (fixed) VS mode - video button is now available to all users of a Studio + - (fixed) A few memory leaks + - (fixed) linux static builds - Version: "1.7.0" Date: 2023-01-20 Description: diff --git a/jacktrip.pro b/jacktrip.pro index d7c56f0..9df06dd 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -5,7 +5,7 @@ CONFIG += c++17 console CONFIG -= app_bundle -CONFIG += qt thread debug_and_release build_all +CONFIG += qt thread debug_and_release build_all qtquickcompiler CONFIG(debug, debug|release) { TARGET = jacktrip_debug application_id = 'org.jacktrip.JackTrip.Devel' @@ -32,6 +32,7 @@ nogui { QT += networkauth QT += qml QT += quick + QT += quickcontrols2 QT += svg QT += websockets } @@ -208,6 +209,7 @@ HEADERS += src/DataProtocol.h \ src/Meter.h \ src/Volume.h \ src/Tone.h \ + src/StereoToMono.h \ src/AudioTester.h \ src/jacktrip_globals.h \ src/jacktrip_types.h \ @@ -246,6 +248,7 @@ HEADERS += src/DataProtocol.h \ src/gui/vuMeter.h !novs { HEADERS += src/gui/virtualstudio.h \ + src/gui/vsInit.h \ src/gui/vsDevice.h \ src/gui/vsAudioInterface.h \ src/gui/vsServerInfo.h \ @@ -277,6 +280,7 @@ SOURCES += src/DataProtocol.cpp \ src/Regulator.cpp \ src/Reverb.cpp \ src/Meter.cpp \ + src/StereoToMono.cpp \ src/Volume.cpp \ src/Tone.cpp \ src/AudioTester.cpp \ @@ -309,6 +313,7 @@ SOURCES += src/DataProtocol.cpp \ src/gui/vuMeter.cpp !novs { SOURCES += src/gui/virtualstudio.cpp \ + src/gui/vsInit.cpp \ src/gui/vsDevice.cpp \ src/gui/vsAudioInterface.cpp \ src/gui/vsServerInfo.cpp \ diff --git a/meson.build b/meson.build index dc19dc2..4ebb6e6 100644 --- a/meson.build +++ b/meson.build @@ -9,7 +9,12 @@ else name_suffix = '' endif -qt5 = import('qt5') +if get_option('qtversion') == '5' + qt = import('qt5') +else + qt = import('qt6') +endif + cmake = import('cmake') compiler = meson.get_compiler('cpp') @@ -38,6 +43,7 @@ src = [ 'src/DataProtocol.cpp', 'src/Meter.cpp', 'src/Volume.cpp', 'src/Tone.cpp', + 'src/StereoToMono.cpp', 'src/Reverb.cpp', 'src/main.cpp', 'src/SslServer.cpp', @@ -47,6 +53,7 @@ moc_h = ['src/DataProtocol.h', 'src/JackTrip.h', 'src/ProcessPlugin.h', 'src/Meter.h', + 'src/StereoToMono.h', 'src/Volume.h', 'src/Tone.h', 'src/JackTripWorker.h', @@ -87,7 +94,11 @@ endif if get_option('nogui') == true defines += '-DNO_GUI' - qt5_dep = dependency('qt5', modules: ['Core', 'Network'], include_type: 'system') + if get_option('qtversion') == '5' + qt_dep = dependency('qt5', modules: ['Core', 'Network'], include_type: 'system') + else + qt_dep = dependency('qt6', modules: ['Core', 'Network'], include_type: 'system') + endif else src += [ 'src/gui/qjacktrip.cpp', @@ -111,11 +122,16 @@ else if get_option('novs') == true defines += '-DNO_VS' - qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') + if get_option('qtversion') == '5' + qt_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') + else + qt_dep = dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') + endif qres = ['src/gui/qjacktrip_novs.qrc'] else src += [ 'src/gui/virtualstudio.cpp', + 'src/gui/vsInit.cpp', 'src/gui/vsDevice.cpp', 'src/gui/vsAudioInterface.cpp', 'src/gui/vsServerInfo.cpp', @@ -128,6 +144,7 @@ else ] moc_h += [ 'src/gui/virtualstudio.h', + 'src/gui/vsInit.h', 'src/gui/vsDevice.h', 'src/gui/vsAudioInterface.h', 'src/gui/vsServerInfo.h', @@ -145,7 +162,11 @@ else moc_h += ['src/gui/vsMacPermissions.h'] endif - qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system') + if get_option('qtversion') == '5' + qt_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'QuickControls2', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system') + else + qt_dep = dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'QuickControls2', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system') + endif qres = ['src/gui/qjacktrip.qrc'] endif @@ -167,9 +188,7 @@ else ui_h += ['src/dblsqd/update_dialog.ui'] endif endif -deps += qt5_dep - -prepro_files = qt5.preprocess(moc_headers : moc_h, ui_files : ui_h, qresources : qres) +deps += qt_dep # TODO: QT_OPENSOURCE should only be defined for open source Qt distribution # in QMake this can be checked with QT_EDITION == 'OpenSource' @@ -206,7 +225,11 @@ if host_machine.system() == 'darwin' and get_option('novs') == false deps += apple_av_dep endif -jacktrip = executable('jacktrip', src, prepro_files, include_directories: incdirs, dependencies: deps, c_args: c_defines, cpp_args: defines, install: true ) +qres_files = qt.compile_resources(sources: qres) +moc_files = qt.compile_moc(headers: moc_h, extra_args: defines) +ui_files = qt.compile_ui(sources: ui_h) + +jacktrip = executable('jacktrip', src, qres_files, ui_files, moc_files, include_directories: incdirs, dependencies: deps, c_args: c_defines, cpp_args: defines, install: true ) help2man = find_program('help2man', required: false) if not (host_machine.system() == 'windows') diff --git a/meson_options.txt b/meson_options.txt index 242ea80..87cd612 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,3 +6,4 @@ option('nogui', type : 'boolean', value : 'false', description: 'Build without g 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') +option('qtversion', type : 'combo', choices: ['5', '6'], description: 'Choose to build with either Qt5 or Qt6') \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index aa3ca43..5e8d7c7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,11 +22,16 @@ nav: - Changelog: About/CHANGELOG.md - License: About/License.md plugins: + - search - macros: include_yaml: - releases: docs/changelog.yml theme: name: material + logo: assets/jacktrip.svg + palette: + primary: black + accent: red features: - navigation.tabs markdown_extensions: diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json index 47fdfc4..fb7033d 100644 --- a/releases/edge/mac-manifests.json +++ b/releases/edge/mac-manifests.json @@ -1,11 +1,61 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.8.0", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0", + "download": { + "date": "2023-03-18T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-macOS-x64-signed-installer.pkg", + "downloadSize": "11771203", + "sha256": "dfee21d5e91a35baaf3b58891188f72f2cb894b2bf796ac70350a2ef9d3fb68c" + } + }, + { + "version": "1.8.0-beta1", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0-beta1", + "download": { + "date": "2023-03-01T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-beta1-macOS-x64-installer.pkg", + "downloadSize": 11761503, + "sha256": "3159d5625d42db7925867949880eceef581f4991959f85bf4203c2ae15fc623f" + } + }, + { + "version": "1.7.1", + "changelog": "Video button, bug fixes, Linux build fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1", + "download": { + "date": "2023-02-10T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.1-macOS-x64-installer.pkg", + "downloadSize": 11679783, + "sha256": "027d1d3aeb4aaca79b21824371a0bfb915b8c43afe924a8ec2be92df719a431f" + } + }, + { + "version": "1.7.1-beta1", + "changelog": "Video button, bug fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1-beta1", + "download": { + "date": "2023-02-03T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.7.1-beta1/JackTrip-v1.7.1-beta1-macOS-x64-installer.pkg", + "downloadSize": 11678822, + "sha256": "1dcc8b54dd67741137582638ce04359404848d5f258c9fae7ab1946730e9381d" + } + }, + { + "version": "1.7.0", + "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0", + "download": { + "date": "2023-01-24T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.0-macOS-x64-installer.pkg", + "downloadSize": 11530594, + "sha256": "0e5731c2ad71aa4bd28ccf9311e312f2386c42b02abbca142777dfb06ea1b427" + } + }, { "version": "1.7.0-rc1", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0-rc1", "download": { - "date": "2022-01-20T00:00:00Z", + "date": "2023-01-20T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.7.0-rc1/JackTrip-v1.7.0-rc1-macOS-x64-installer.pkg", "downloadSize": 11530680, "sha256": "406134ee2017bcb762f968f893bc28463149d7567dd33b92f963ca9e09608636" @@ -15,7 +65,7 @@ "version": "1.6.9-beta3", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta3", "download": { - "date": "2022-01-18T00:00:00Z", + "date": "2023-01-18T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.9-beta3/JackTrip-v1.6.9-beta3-macOS-x64-installer.pkg", "downloadSize": 11528836, "sha256": "30689d83641377c1594e1db44d8e6cf75a45780969381d02c11ede81a175561f" @@ -25,7 +75,7 @@ "version": "1.6.9-beta2", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta2", "download": { - "date": "2022-01-10T00:00:00Z", + "date": "2023-01-10T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.9-beta2/JackTrip-v1.6.9-beta2-macOS-x64-installer.pkg", "downloadSize": 11528427, "sha256": "884e5c0cf3ea5bc82b348a739df3ba11238074a9552dcb1d1f250f484be89b77" diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json index 46ff9f2..0fa745b 100644 --- a/releases/edge/win-manifests.json +++ b/releases/edge/win-manifests.json @@ -1,11 +1,61 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.8.0", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0", + "download": { + "date": "2023-03-18T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-Windows-x64-signed-installer.msi", + "downloadSize": "45699072", + "sha256": "4b6705a2e8af7f9a516fefaf119d5e6cadf93e8f40964cd52b635ced5745b267" + } + }, + { + "version": "1.8.0-beta1", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0-beta1", + "download": { + "date": "2023-03-01T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-beta1-Windows-x64-installer.msi", + "downloadSize": 45568000, + "sha256": "61641b72fe27389ab755580d5b94fa2de993ad42967af0c5c765743ca8b30602" + } + }, + { + "version": "1.7.1", + "changelog": "Video button, bug fixes, Linux build fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1", + "download": { + "date": "2023-02-10T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.1-Windows-x64-installer.msi", + "downloadSize": 45330432, + "sha256": "fb5d756afcd471ca8ae45b05b411235026f23f2178893d85ac12f39c3f66a01a" + } + }, + { + "version": "1.7.1-beta1", + "changelog": "Video button, bug fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1-beta1", + "download": { + "date": "2023-02-03T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.7.1-beta1/JackTrip-v1.7.1-beta1-Windows-x64-installer.msi", + "downloadSize": 45326336, + "sha256": "eeb16bc11957413fb74da852b9e0fa3cb8c84cb2945d5c992a40f3e9b526c294" + } + }, + { + "version": "1.7.0", + "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0", + "download": { + "date": "2023-01-24T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.0-Windows-x64-installer.msi", + "downloadSize": 44572672, + "sha256": "a1890fe10de484f423a17118031d898abacc9b9eb2ccd35bdb4351e9411ff866" + } + }, { "version": "1.7.0-rc1", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0-rc1", "download": { - "date": "2022-01-20T00:00:00Z", + "date": "2023-01-20T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.7.0-rc1/JackTrip-v1.7.0-rc1-Windows-x64-installer.msi", "downloadSize": 44556288, "sha256": "edba383791a598954d129d39024e87f3c062985d10f47dbea43f3d226ee37c6c" @@ -15,7 +65,7 @@ "version": "1.6.9-beta3", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta3", "download": { - "date": "2022-01-10T00:00:00Z", + "date": "2023-01-10T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.9-beta3/JackTrip-v1.6.9-beta3-Windows-x64-installer.msi", "downloadSize": 44552192, "sha256": "f0d8157d99da5ecfa3fb21e6bb039ea48052fc0792b114ca4f40ae0a554a4852" @@ -25,7 +75,7 @@ "version": "1.6.9-beta2", "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta2", "download": { - "date": "2022-01-10T00:00:00Z", + "date": "2023-01-10T00:00:00Z", "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.9-beta2/JackTrip-v1.6.9-beta2-Windows-x64-installer.msi", "downloadSize": 44548096, "sha256": "a8e7c9b353d953df894a827e2856baa71f89dc8fa835a5f3eb8422a94ffff5b5" diff --git a/releases/stable/linux-manifests.json b/releases/stable/linux-manifests.json index 96b9b59..4b4156c 100644 --- a/releases/stable/linux-manifests.json +++ b/releases/stable/linux-manifests.json @@ -1,6 +1,36 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.8.0", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0", + "download": { + "date": "2023-03-18T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-Linux-x64-binary.zip", + "downloadSize": "36167636", + "sha256": "6f14273ffd5526d576a184f4559adb4124def8760d0b9ba60c43b0fa75b2d1a5" + } + }, + { + "version": "1.7.1", + "changelog": "Video button, bug fixes, Linux build fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1", + "download": { + "date": "2023-02-10T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.1-Linux-x64-binary.zip", + "downloadSize": 34338503, + "sha256": "4298e1edd561815630e3c6573660eeea15b199a12742ab1b90d43a6e3522b632" + } + }, + { + "version": "1.7.0", + "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0", + "download": { + "date": "2023-01-24T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.0-Linux-x64-binary.zip", + "downloadSize": 32554865, + "sha256": "13b3781f6dca0713eb135c9352c23cf0f40094603357ee5d5a940868597e8bb8" + } + }, { "version": "1.6.8", "changelog": "Critical bug fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.8", diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json index 65af705..c94527a 100644 --- a/releases/stable/mac-manifests.json +++ b/releases/stable/mac-manifests.json @@ -1,6 +1,36 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.8.0", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0", + "download": { + "date": "2023-03-18T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-macOS-x64-signed-installer.pkg", + "downloadSize": "11771203", + "sha256": "dfee21d5e91a35baaf3b58891188f72f2cb894b2bf796ac70350a2ef9d3fb68c" + } + }, + { + "version": "1.7.1", + "changelog": "Video button, bug fixes, Linux build fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1", + "download": { + "date": "2023-02-10T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.1-macOS-x64-installer.pkg", + "downloadSize": 11679783, + "sha256": "027d1d3aeb4aaca79b21824371a0bfb915b8c43afe924a8ec2be92df719a431f" + } + }, + { + "version": "1.7.0", + "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0", + "download": { + "date": "2023-01-24T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.0-macOS-x64-installer.pkg", + "downloadSize": 11530594, + "sha256": "0e5731c2ad71aa4bd28ccf9311e312f2386c42b02abbca142777dfb06ea1b427" + } + }, { "version": "1.6.8", "changelog": "Critical bug fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.8", diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json index be118aa..38b892f 100644 --- a/releases/stable/win-manifests.json +++ b/releases/stable/win-manifests.json @@ -1,6 +1,36 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.8.0", + "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0", + "download": { + "date": "2023-03-18T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-Windows-x64-signed-installer.msi", + "downloadSize": "45699072", + "sha256": "4b6705a2e8af7f9a516fefaf119d5e6cadf93e8f40964cd52b635ced5745b267" + } + }, + { + "version": "1.7.1", + "changelog": "Video button, bug fixes, Linux build fixes, and Qt upgrades: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.1", + "download": { + "date": "2023-02-10T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.1-Windows-x64-installer.msi", + "downloadSize": 45330432, + "sha256": "fb5d756afcd471ca8ae45b05b411235026f23f2178893d85ac12f39c3f66a01a" + } + }, + { + "version": "1.7.0", + "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.7.0", + "download": { + "date": "2023-01-24T00:00:00Z", + "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.7.0-Windows-x64-installer.msi", + "downloadSize": 44572672, + "sha256": "a1890fe10de484f423a17118031d898abacc9b9eb2ccd35bdb4351e9411ff866" + } + }, { "version": "1.6.8", "changelog": "Critical bug fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.8", diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 29bb308..c91ecfd 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -47,15 +47,16 @@ using std::cout; using std::endl; //******************************************************************************* -AudioInterface::AudioInterface(JackTrip* jacktrip, int NumInChans, int NumOutChans, +AudioInterface::AudioInterface(QVarLengthArray InputChans, + QVarLengthArray OutputChans, + inputMixModeT InputMixMode, #ifdef WAIR // wair int NumNetRevChans, #endif // endwhere audioBitResolutionT AudioBitResolution, - bool processWithNetwork) - : mJackTrip(jacktrip) - , mNumInChans(NumInChans) - , mNumOutChans(NumOutChans) + bool processWithNetwork, JackTrip* jacktrip) + : mInputChans(InputChans) + , mOutputChans(OutputChans) , #ifdef WAIR // WAIR mNumNetRevChans(NumNetRevChans) @@ -69,24 +70,28 @@ AudioInterface::AudioInterface(JackTrip* jacktrip, int NumInChans, int NumOutCha , mAudioOutputPacket(NULL) , mLoopBack(false) , mProcessWithNetwork(processWithNetwork) + , mJackTrip(jacktrip) + , mInputMixMode(InputMixMode) , mProcessingAudio(false) { #ifndef WAIR // cc // Initialize and assign memory for ProcessPlugins Buffers - mInProcessBuffer.resize(mNumInChans); - mOutProcessBuffer.resize(mNumOutChans); + mInProcessBuffer.resize(mInputChans.size()); + mOutProcessBuffer.resize(mOutputChans.size()); // Set pointer to NULL - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < mInProcessBuffer.size(); i++) { mInProcessBuffer[i] = NULL; } - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < mOutProcessBuffer.size(); i++) { mOutProcessBuffer[i] = NULL; } #else // WAIR - int iCnt = (mNumInChans > mNumNetRevChans) ? mNumInChans : mNumNetRevChans; - int oCnt = (mNumOutChans > mNumNetRevChans) ? mNumOutChans : mNumNetRevChans; - int aCnt = (mNumNetRevChans) ? mNumInChans : 0; + int iCnt = + (mInputChans.size() > mNumNetRevChans) ? mInputChans.size() : mNumNetRevChans; + int oCnt = + (mOutputChans.size() > mNumNetRevChans) ? mOutputChans.size() : mNumNetRevChans; + int aCnt = (mNumNetRevChans) ? mInputChans.size() : 0; for (int i = 0; i < iCnt; i++) { mInProcessBuffer[i] = NULL; } @@ -98,11 +103,14 @@ AudioInterface::AudioInterface(JackTrip* jacktrip, int NumInChans, int NumOutCha } #endif // endwhere - mInBufCopy.resize(mNumInChans); - for (int i = 0; i < mNumInChans; i++) { + mInBufCopy.resize(mInputChans.size()); + for (int i = 0; i < mInputChans.size(); i++) { mInBufCopy[i] = new sample_t[MAX_AUDIO_BUFFER_SIZE]; // required for processing audio input } + + mNumInChans = mInputChans.size(); + mNumOutChans = mOutputChans.size(); } //******************************************************************************* @@ -111,35 +119,31 @@ AudioInterface::~AudioInterface() delete[] mAudioInputPacket; delete[] mAudioOutputPacket; #ifndef WAIR // NOT WAIR: - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < mInProcessBuffer.size(); i++) { delete[] mInProcessBuffer[i]; } - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < mOutProcessBuffer.size(); i++) { delete[] mOutProcessBuffer[i]; } #else // WAIR - int iCnt = (mNumInChans > mNumNetRevChans) ? mNumInChans : mNumNetRevChans; - int oCnt = (mNumOutChans > mNumNetRevChans) ? mNumOutChans : mNumNetRevChans; - int aCnt = (mNumNetRevChans) ? mNumInChans : 0; - for (int i = 0; i < iCnt; i++) { + for (int i = 0; i < mInProcessBuffer.size(); i++) { delete[] mInProcessBuffer[i]; } - for (int i = 0; i < oCnt; i++) { + for (int i = 0; i < mOutProcessBuffer.size(); i++) { delete[] mOutProcessBuffer[i]; } - for (int i = 0; i < aCnt; i++) { + for (int i = 0; i < mAPInBuffer.size(); i++) { delete[] mAPInBuffer[i]; } #endif // endwhere - for (auto* i : qAsConst(mProcessPluginsFromNetwork)) { delete i; } for (auto* i : qAsConst(mProcessPluginsToNetwork)) { delete i; } - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < mInBufCopy.size(); i++) { delete[] mInBufCopy[i]; } } @@ -147,11 +151,18 @@ AudioInterface::~AudioInterface() //******************************************************************************* void AudioInterface::setup(bool /*verbose*/) { + int nChansIn = mInputChans.size(); + int nChansOut = mOutputChans.size(); + inputMixModeT inputMixMode = mInputMixMode; + if (inputMixMode == MIXTOMONO) { + nChansIn = 1; + } + // Allocate buffer memory to read and write mSizeInBytesPerChannel = getSizeInBytesPerChannel(); - int size_audio_input = mSizeInBytesPerChannel * getNumInputChannels(); - int size_audio_output = mSizeInBytesPerChannel * getNumOutputChannels(); + int size_audio_input = mSizeInBytesPerChannel * nChansIn; + int size_audio_output = mSizeInBytesPerChannel * nChansOut; #ifdef WAIR // WAIR if (mNumNetRevChans) // else don't change sizes { @@ -167,40 +178,40 @@ void AudioInterface::setup(bool /*verbose*/) if (mNumNetRevChans) { mInProcessBuffer.resize(mNumNetRevChans); mOutProcessBuffer.resize(mNumNetRevChans); - mAPInBuffer.resize(mNumInChans); + mAPInBuffer.resize(nChansIn); mNetInBuffer.resize(mNumNetRevChans); } else // don't change sizes #endif // endwhere { - mInProcessBuffer.resize(mNumInChans); - mOutProcessBuffer.resize(mNumOutChans); + mInProcessBuffer.resize(nChansIn); + mOutProcessBuffer.resize(nChansOut); } int nframes = getBufferSizeInSamples(); #ifndef WAIR // NOT WAIR: - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < nChansIn; i++) { mInProcessBuffer[i] = new sample_t[nframes]; // set memory to 0 std::memset(mInProcessBuffer[i], 0, sizeof(sample_t) * nframes); } - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < nChansOut; i++) { mOutProcessBuffer[i] = new sample_t[nframes]; // set memory to 0 std::memset(mOutProcessBuffer[i], 0, sizeof(sample_t) * nframes); } #else // WAIR - for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : mNumInChans); i++) { + for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : nChansIn); i++) { mInProcessBuffer[i] = new sample_t[nframes]; // set memory to 0 std::memset(mInProcessBuffer[i], 0, sizeof(sample_t) * nframes); } - for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : mNumOutChans); i++) { + for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : nChansOut); i++) { mOutProcessBuffer[i] = new sample_t[nframes]; // set memory to 0 std::memset(mOutProcessBuffer[i], 0, sizeof(sample_t) * nframes); } - for (int i = 0; i < ((mNumNetRevChans) ? mNumInChans : 0); i++) { + for (int i = 0; i < ((mNumNetRevChans) ? nChansIn : 0); i++) { mAPInBuffer[i] = new sample_t[nframes]; // set memory to 0 std::memset(mAPInBuffer[i], 0, sizeof(sample_t) * nframes); @@ -224,6 +235,11 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, QVarLengthArray& out_buffer, unsigned int n_frames) { + int nChansIn = mInputChans.size(); + inputMixModeT inputMixMode = mInputMixMode; + if (inputMixMode == MIXTOMONO) { + nChansIn = 1; + } // Allocate the Process Callback //------------------------------------------------------------------- // 1) First, process incoming packets @@ -271,10 +287,10 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, } } #else // WAIR: - for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : mNumOutChans); i++) { + for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : nChansOut); i++) { std::memset(mOutProcessBuffer[i], 0, sizeof(sample_t) * n_frames); } - for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : mNumInChans); i++) { + for (int i = 0; i < ((mNumNetRevChans) ? mNumNetRevChans : nChansIn); i++) { std::memset(mInProcessBuffer[i], 0, sizeof(sample_t) * n_frames); if (mNumNetRevChans) { if (client) @@ -300,7 +316,7 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, int nop = mProcessPluginsToNetwork.size(); // number of OUTGOING processing modules if (nop > 0 || audioTesting) { // cannot modify in_buffer, so make a copy // in_buffer is "in" from local audio hardware via JACK - if (mInBufCopy.size() < mNumInChans) { // created in constructor above + if (mInBufCopy.size() < nChansIn) { // created in constructor above std::cerr << "*** AudioInterface.cpp: Number of Input Channels changed - " "insufficient room reserved\n"; exit(1); @@ -310,7 +326,7 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, << " larger than expected max = " << MAX_AUDIO_BUFFER_SIZE << "\n"; exit(1); } - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < nChansIn; i++) { std::memcpy(mInBufCopy[i], in_buffer[i], sizeof(sample_t) * n_frames); } for (int i = 0; i < nop; i++) { @@ -345,11 +361,11 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, #define AP #ifndef AP // straight to audio out - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < nChansOut; i++) { std::memset(out_buffer[i], 0, sizeof(sample_t) * n_frames); } for (int i = 0; i < mNumNetRevChans; i++) { - sample_t* mix_sample = out_buffer[i % mNumOutChans]; + sample_t* mix_sample = out_buffer[i % nChansOut]; sample_t* tmp_sample = mNetInBuffer[i]; // mNetInBuffer for (int j = 0; j < (int)n_frames; j++) { mix_sample[j] += tmp_sample[j]; @@ -360,17 +376,17 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, // output through all-pass cascade // AP2 is 2 channel, mixes inputs to mono, then splits to two parallel AP chains // AP8 is 2 channel, two parallel AP chains - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < nChansIn; i++) { std::memset(mAPInBuffer[i], 0, sizeof(sample_t) * n_frames); } for (int i = 0; i < mNumNetRevChans; i++) { - sample_t* mix_sample = mAPInBuffer[i % mNumOutChans]; + sample_t* mix_sample = mAPInBuffer[i % nChansOut]; sample_t* tmp_sample = mNetInBuffer[i]; for (int j = 0; j < n_frames; j++) { mix_sample[j] += tmp_sample[j]; } } // nib16 to apib2 - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < nChansOut; i++) { std::memset(out_buffer[i], 0, sizeof(sample_t) * n_frames); } mProcessPluginsFromNetwork[APDSP]->compute(n_frames, mAPInBuffer.data(), @@ -379,7 +395,7 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, //#define ADD_DIRECT #ifdef ADD_DIRECT - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < nChansIn; i++) { sample_t* mix_sample = out_buffer[i]; sample_t* tmp_sample = in_buffer[i]; for (int j = 0; j < n_frames; j++) { @@ -412,20 +428,22 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, void AudioInterface::broadcastCallback(QVarLengthArray& mon_buffer, unsigned int n_frames) { + int nChansOut = mOutputChans.size(); + /// \todo cast *mInBuffer[i] to the bit resolution // Output Process (from NETWORK to JACK) // ---------------------------------------------------------------- // Read Audio buffer from RingBuffer (read from incoming packets) mJackTrip->receiveBroadcastPacket(mAudioOutputPacket); // Extract separate channels to send to Jack - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < nChansOut; i++) { sample_t* tmp_sample = mon_buffer[i]; // sample buffer for channel i for (unsigned int j = 0; j < n_frames; j++) { // Change the bit resolution on each sample fromBitToSampleConversion( // use interleaved channel layout //&mOutputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)], - &mAudioOutputPacket[(j * mBitResolutionMode * mNumOutChans) + &mAudioOutputPacket[(j * mBitResolutionMode * nChansOut) + (i * mBitResolutionMode)], &tmp_sample[j], mBitResolutionMode); } @@ -439,6 +457,8 @@ void AudioInterface::broadcastCallback(QVarLengthArray& mon_buffer, void AudioInterface::computeProcessFromNetwork(QVarLengthArray& out_buffer, unsigned int n_frames) { + int nChansOut = mOutputChans.size(); + /// \todo cast *mInBuffer[i] to the bit resolution // Output Process (from NETWORK to JACK) // ---------------------------------------------------------------- @@ -455,7 +475,7 @@ void AudioInterface::computeProcessFromNetwork(QVarLengthArray& out_b fromBitToSampleConversion( // use interleaved channel layout //&mOutputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)], - &mOutputPacket[(j * mBitResolutionMode * mNumOutChans) + &mOutputPacket[(j * mBitResolutionMode * nChansOut) + (i * mBitResolutionMode)], &tmp_sample[j], mBitResolutionMode); } @@ -464,7 +484,7 @@ void AudioInterface::computeProcessFromNetwork(QVarLengthArray& out_b #endif // endwhere // Extract separate channels to send to Jack - for (int i = 0; i < mNumOutChans; i++) { + for (int i = 0; i < nChansOut; i++) { //-------- // This should be faster for 32 bits // std::memcpy(mOutBuffer[i], &mOutputPacket[i*mSizeInBytesPerChannel], @@ -476,7 +496,7 @@ void AudioInterface::computeProcessFromNetwork(QVarLengthArray& out_b fromBitToSampleConversion( // use interleaved channel layout //&mOutputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)], - &mAudioOutputPacket[(j * mBitResolutionMode * mNumOutChans) + &mAudioOutputPacket[(j * mBitResolutionMode * nChansOut) + (i * mBitResolutionMode)], &tmp_sample[j], mBitResolutionMode); } @@ -487,6 +507,11 @@ void AudioInterface::computeProcessFromNetwork(QVarLengthArray& out_b void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buffer, unsigned int n_frames) { + int nChansIn = mInputChans.size(); + inputMixModeT inputMixMode = mInputMixMode; + if (inputMixMode == MIXTOMONO) { + nChansIn = 1; + } // Input Process (from JACK to NETWORK) // ---------------------------------------------------------------- // Concatenate all the channels from jack to form packet @@ -495,7 +520,7 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff if (mNumNetRevChans) for (int i = 0; i < mNumNetRevChans; i++) { sample_t* tmp_sample = - in_buffer[i % mNumInChans]; // sample buffer for channel i + in_buffer[i % nChansIn]; // sample buffer for channel i sample_t* tmp_process_sample = mInProcessBuffer[i]; // sample buffer from the output process sample_t tmp_result; @@ -511,7 +536,7 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff &tmp_result, // use interleaved channel layout //&mInputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)], - &mInputPacket[(j * mBitResolutionMode * mNumOutChans) + &mInputPacket[(j * mBitResolutionMode * nChansOut) + (i * mBitResolutionMode)], mBitResolutionMode); } @@ -519,7 +544,7 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff else // not wair #endif // endwhere - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < nChansIn; i++) { //-------- // This should be faster for 32 bits // std::memcpy(&mInputPacket[i*mSizeInBytesPerChannel], mInBuffer[i], @@ -538,7 +563,7 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff &tmp_result, // use interleaved channel layout //&mInputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)], - &mAudioInputPacket[(j * mBitResolutionMode * mNumInChans) + &mAudioInputPacket[(j * mBitResolutionMode * nChansIn) + (i * mBitResolutionMode)], mBitResolutionMode); } @@ -650,9 +675,16 @@ void AudioInterface::appendProcessPluginToNetwork(ProcessPlugin* plugin) if (not plugin) { return; } + + int nChansIn = mInputChans.size(); + inputMixModeT inputMixMode = mInputMixMode; + if (inputMixMode == MIXTOMONO) { + nChansIn = 1; + } + int nTestChans = (mAudioTesterP && mAudioTesterP->getEnabled()) ? 1 : 0; - int nPluginChans = mNumInChans - nTestChans; - assert(nTestChans == 0 || (mAudioTesterP->getSendChannel() == mNumInChans - 1)); + int nPluginChans = nChansIn - nTestChans; + assert(nTestChans == 0 || (mAudioTesterP->getSendChannel() == nChansIn - 1)); if (plugin->getNumInputs() < nPluginChans) { std::cerr << "*** AudioInterface.cpp: appendProcessPluginToNetwork: ProcessPlugin " @@ -669,9 +701,12 @@ void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin) if (not plugin) { return; } + + int nChansOut = mOutputChans.size(); + int nTestChans = (mAudioTesterP && mAudioTesterP->getEnabled()) ? 1 : 0; - int nPluginChans = mNumOutChans - nTestChans; - assert(nTestChans == 0 || (mAudioTesterP->getSendChannel() == mNumOutChans - 1)); + int nPluginChans = nChansOut - nTestChans; + assert(nTestChans == 0 || (mAudioTesterP->getSendChannel() == nChansOut - 1)); if (plugin->getNumOutputs() > nPluginChans) { std::cerr << "*** AudioInterface.cpp: appendProcessPluginFromNetwork: ProcessPlugin " @@ -685,6 +720,13 @@ void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin) void AudioInterface::initPlugins(bool verbose) { + int nChansIn = mInputChans.size(); + int nChansOut = mOutputChans.size(); + inputMixModeT inputMixMode = mInputMixMode; + if (inputMixMode == MIXTOMONO) { + nChansIn = 1; + } + int nPlugins = mProcessPluginsFromNetwork.size() + mProcessPluginsToNetwork.size(); if (nPlugins > 0) { if (verbose) { @@ -694,12 +736,12 @@ void AudioInterface::initPlugins(bool verbose) for (ProcessPlugin* plugin : qAsConst(mProcessPluginsFromNetwork)) { plugin->setOutgoingToNetwork(false); - plugin->updateNumChannels(mNumInChans, mNumOutChans); + plugin->updateNumChannels(nChansIn, nChansOut); plugin->init(mSampleRate); } for (ProcessPlugin* plugin : qAsConst(mProcessPluginsToNetwork)) { plugin->setOutgoingToNetwork(true); - plugin->updateNumChannels(mNumInChans, mNumOutChans); + plugin->updateNumChannels(nChansIn, nChansOut); plugin->init(mSampleRate); } } @@ -776,8 +818,9 @@ void AudioInterface::setDevicesWarningMsg(warningMessageT msg) switch (msg) { case DEVICE_WARN_LATENCY: mWarningMsg = - "The selected devices don't support low latency. You can use them, but you " - "will experience audio delay. Make sure you have up to date drivers from the " + "The currently selected devices don't support low latency. You can use them, " + "but you " + "may experience audio delay. Make sure you have up to date drivers from the " "manufacturer!"; #ifdef _WIN32 mWarningHelpUrl = "https://help.jacktrip.org/hc/en-us/articles/4409919243155"; @@ -851,4 +894,4 @@ std::string AudioInterface::getDevicesWarningHelpUrl() std::string AudioInterface::getDevicesErrorHelpUrl() { return mErrorHelpUrl; -} \ No newline at end of file +} diff --git a/src/AudioInterface.h b/src/AudioInterface.h index 20967d6..b5ad1be 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -86,19 +86,28 @@ class AudioInterface DEVICE_ERR_NO_DEVICES }; + enum inputMixModeT : int { + MIX_UNSET = 0, + MONO = 1, + STEREO = 2, + MIXTOMONO = 3, + }; + /** \brief The class constructor - * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) * \param NumInChans Number of Input Channels * \param NumOutChans Number of Output Channels * \param AudioBitResolution Audio Sample Resolutions in bits + * \param processWithNetwork Send audio to and from the network + * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) */ AudioInterface( - JackTrip* jacktrip, int NumInChans, int NumOutChans, + QVarLengthArray InputChans, QVarLengthArray OutputChans, + inputMixModeT InputMixMode, #ifdef WAIR // wair int NumNetRevChans, #endif // endwhere AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16, - bool processWithNetwork = true); + bool processWithNetwork = false, JackTrip* jacktrip = nullptr); /// \brief The class destructor virtual ~AudioInterface(); @@ -173,8 +182,17 @@ class AudioInterface const AudioInterface::audioBitResolutionT sourceBitResolution); //--------------SETTERS--------------------------------------------- - virtual void setNumInputChannels(int nchannels) { mNumInChans = nchannels; } - virtual void setNumOutputChannels(int nchannels) { mNumOutChans = nchannels; } + virtual void setInputChannels(QVarLengthArray inputChans) + { + mInputChans = inputChans; + mNumInChans = inputChans.size(); + } + virtual void setOutputChannels(QVarLengthArray outputChans) + { + mOutputChans = outputChans; + mNumOutChans = outputChans.size(); + } + virtual void setInputMixMode(inputMixModeT mode) { mInputMixMode = mode; } virtual void setSampleRate(uint32_t sample_rate) { mSampleRate = sample_rate; } virtual void setBufferSize(uint32_t buffersize) { mBufferSizeInSamples = buffersize; } virtual void setDeviceID(uint32_t device_id) { mDeviceID = device_id; } @@ -199,9 +217,12 @@ class AudioInterface //--------------GETTERS--------------------------------------------- /// \brief Get Number of Input Channels - virtual int getNumInputChannels() const { return mNumInChans; } + virtual int getNumInputChannels() const { return mInputChans.size(); } /// \brief Get Number of Output Channels - virtual int getNumOutputChannels() const { return mNumOutChans; } + virtual int getNumOutputChannels() const { return mOutputChans.size(); } + virtual QVarLengthArray getInputChannels() const { return mInputChans; } + virtual QVarLengthArray getOutputChannels() const { return mOutputChans; } + virtual inputMixModeT getInputMixMode() const { return mInputMixMode; } virtual uint32_t getBufferSizeInSamples() const { return mBufferSizeInSamples; } virtual uint32_t getDeviceID() const { return mDeviceID; } virtual std::string getInputDevice() const { return mInputDeviceName; } @@ -238,9 +259,8 @@ class AudioInterface void computeProcessToNetwork(QVarLengthArray& in_buffer, unsigned int n_frames); - JackTrip* mJackTrip; ///< JackTrip Mediator Class pointer - int mNumInChans; ///< Number of Input Channels - int mNumOutChans; ///< Number of Output Channels + QVarLengthArray mInputChans; + QVarLengthArray mOutputChans; #ifdef WAIR // wair int mNumNetRevChans; ///< Number of Network Audio Channels (net comb filters) QVarLengthArray @@ -275,6 +295,11 @@ class AudioInterface AudioTester* mAudioTesterP{nullptr}; protected: + JackTrip* mJackTrip; ///< JackTrip Mediator Class pointer + int mNumInChans; ///< Number of Input Channels + int mNumOutChans; ///< Number of Output Channels + inputMixModeT mInputMixMode; ///< Input mixing mode + void setDevicesWarningMsg(warningMessageT msg); void setDevicesErrorMsg(errorMessageT msg); diff --git a/src/Auth.cpp b/src/Auth.cpp index c23771c..3b59809 100644 --- a/src/Auth.cpp +++ b/src/Auth.cpp @@ -39,22 +39,31 @@ #include #include #include +#include #include #include #include -Auth::Auth(const QString& fileName, QObject* parent) +Auth::Auth(const QString& fileName, bool monitorChanges, QObject* parent) : QObject(parent) , m_days({"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}) , m_authFileName(fileName) + , m_monitorChanges(monitorChanges) { - // Load our credentials file. - loadAuthFile(m_authFileName); + if (!m_authFileName.isEmpty()) { + // Load our credentials file. + loadAuthFile(m_authFileName); - // Monitor the file for any changes. (Reload when it does.) - m_authFileWatcher.addPath(m_authFileName); - QObject::connect(&m_authFileWatcher, &QFileSystemWatcher::fileChanged, this, - &Auth::reloadAuthFile, Qt::QueuedConnection); + // Monitor the file for any changes. (Reload when it does.) + if (m_monitorChanges) { + m_authFileWatcher.addPath(m_authFileName); + } + } + + if (m_monitorChanges) { + QObject::connect(&m_authFileWatcher, &QFileSystemWatcher::fileChanged, this, + &Auth::reloadAuthFile, Qt::QueuedConnection); + } } Auth::AuthResponseT Auth::checkCredentials(const QString& username, @@ -81,6 +90,91 @@ Auth::AuthResponseT Auth::checkCredentials(const QString& username, return WRONGCREDS; } +bool Auth::setFileName(const QString& fileName, bool readFile) +{ + if (m_monitorChanges) { + if (!m_authFileName.isEmpty()) { + m_authFileWatcher.removePath(m_authFileName); + } + m_authFileWatcher.addPath(fileName); + } + m_authFileName = fileName; + // If we're monitoring for changes, then we read the file regardless of the readFile + // setting. + if (readFile || m_monitorChanges) { + return loadAuthFile(m_authFileName); + } else { + m_passwordTable.clear(); + m_timesTable.clear(); + return true; + } +} + +QStringList Auth::getUsers() +{ + return m_passwordTable.keys(); +} + +QString Auth::getTimes(const QString& username) +{ + return m_timesTable[username]; +} + +void Auth::deleteUser(const QString& username) +{ + m_passwordTable.remove(username); + m_timesTable.remove(username); +} + +void Auth::editUser(const QString& username, const QString& times, + const QString& password) +{ + // Can't add a new user if the password isn't set. + if (!m_passwordTable.contains(username) && password.isEmpty()) { + return; + } + + if (!password.isEmpty()) { + QString hashed = generateSha512Hash(password, generateSalt()); + m_passwordTable[username] = hashed; + } + + if (verifyTimeFormat(times)) { + m_timesTable[username] = times; + } else { + std::cout << "WARNING: Not writing invalid time string (" << times.toStdString() + << ") for user " << username.toStdString(); + } +} + +bool Auth::writeFile() +{ + QFile file(m_authFileName); + if (file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + QTextStream output(&file); + QHashIterator iterator(m_passwordTable); + while (iterator.hasNext()) { + iterator.next(); + output << QStringLiteral("%1:%2:%3\n") + .arg(iterator.key(), iterator.value(), + m_timesTable[iterator.key()]); + } + file.close(); + } else { + return false; + } + return true; +} + +QString Auth::generateSalt() +{ + QString result; + for (int i = 0; i < 16; i++) { + result.append(char64(QRandomGenerator::global()->bounded(0, 64))); + } + return result; +} + void Auth::reloadAuthFile() { // Some text editors will replace the original file instead of modifying the existing @@ -91,13 +185,14 @@ void Auth::reloadAuthFile() loadAuthFile(m_authFileName); } -void Auth::loadAuthFile(const QString& filename) +bool Auth::loadAuthFile(const QString& filename) { - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) { - m_passwordTable.clear(); - m_timesTable.clear(); + m_passwordTable.clear(); + m_timesTable.clear(); + bool errorFree = true; + QFile file(filename); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { // Read our file into our password table QTextStream input(&file); int lineNumber = 0; @@ -106,9 +201,14 @@ void Auth::loadAuthFile(const QString& filename) QStringList lineParts = input.readLine().split(QStringLiteral(":")); if (lineParts.count() < 3) { // We don't have a correctly formatted line. Ignore it. - std::cout - << "WARNING: Incorrectly formatted line in auth file ignored. (Line " - << lineNumber << ")" << std::endl; + errorFree = false; + if (m_monitorChanges) { + // Don't output warnings if we're not monitoring changes. + // (This is the case when we're loading it into the editor.) + std::cout << "WARNING: Incorrectly formatted line in auth file " + "ignored. (Line " + << lineNumber << ")" << std::endl; + } continue; } @@ -125,8 +225,11 @@ void Auth::loadAuthFile(const QString& filename) invalid = true; } if (invalid) { - std::cout << "WARNING: Invalid password hash in auth file. (Line " - << lineNumber << ")" << std::endl; + errorFree = false; + if (m_monitorChanges) { + std::cout << "WARNING: Invalid password hash in auth file. (Line " + << lineNumber << ")" << std::endl; + } continue; } @@ -135,6 +238,7 @@ void Auth::loadAuthFile(const QString& filename) } file.close(); } + return errorFree; } bool Auth::checkTime(const QString& username) @@ -183,6 +287,38 @@ bool Auth::checkTime(const QString& username) return false; } +bool Auth::verifyTimeFormat(const QString& timeString) +{ + QStringList times = timeString.split(QStringLiteral(",")); + if (times.count() == 1 && times.at(0).isEmpty()) { + return true; + } + + for (int i = 0; i < times.count(); i++) { + if (times.at(i) == QLatin1String("*")) { + continue; + } + if (m_days.contains(times.at(i).left(2))) { + QString accessTime = QString(times.at(i)).remove(0, 2); + if (accessTime == QLatin1String("*")) { + continue; + } + QStringList range = accessTime.split(QStringLiteral("-")); + if (range.count() == 2) { + QTime start = QTime::fromString(range.at(0), QStringLiteral("hhmm")); + QTime end = QTime::fromString(range.at(1), QStringLiteral("hhmm")); + + if (!start.isValid() || !end.isValid()) { + return false; + } + } + } else { + return false; + } + } + return true; +} + char Auth::char64(int value) { // Returns a base 64 encoding using the following characters: diff --git a/src/Auth.h b/src/Auth.h index 712f524..10cff27 100644 --- a/src/Auth.h +++ b/src/Auth.h @@ -54,17 +54,28 @@ class Auth : public QObject WRONGTIME = 5 << 16 }; - Auth(const QString& fileName, QObject* parent = nullptr); + Auth(const QString& fileName = "", bool monitorChanges = false, + QObject* parent = nullptr); ~Auth(); AuthResponseT checkCredentials(const QString& username, const QString& password); + bool setFileName(const QString& fileName, bool readFile = true); + QStringList getUsers(); + QString getTimes(const QString& username); + void deleteUser(const QString& username); + void editUser(const QString& username, const QString& times, + const QString& password = ""); + bool writeFile(); + + QString generateSalt(); private slots: void reloadAuthFile(); private: - void loadAuthFile(const QString& filename); + bool loadAuthFile(const QString& filename); bool checkTime(const QString& username); + bool verifyTimeFormat(const QString& times); char char64(int value); QByteArray charGroup(unsigned char byte3, unsigned char byte2, unsigned char byte1, @@ -77,6 +88,7 @@ class Auth : public QObject QHash m_timesTable; QString m_authFileName; + bool m_monitorChanges; QFileSystemWatcher m_authFileWatcher; }; diff --git a/src/JackAudioInterface.cpp b/src/JackAudioInterface.cpp index fd02495..62d9459 100644 --- a/src/JackAudioInterface.cpp +++ b/src/JackAudioInterface.cpp @@ -62,55 +62,21 @@ QMutex JackAudioInterface::sJackMutex; //******************************************************************************* JackAudioInterface::JackAudioInterface( - JackTrip* jacktrip, int NumInChans, int NumOutChans, + QVarLengthArray InputChans, QVarLengthArray OutputChans, #ifdef WAIR // wair int NumNetRevChans, #endif // endwhere - AudioInterface::audioBitResolutionT AudioBitResolution, const QString& ClientName) - : AudioInterface(jacktrip, NumInChans, NumOutChans, + AudioInterface::audioBitResolutionT AudioBitResolution, bool processWithNetwork, + JackTrip* jacktrip, const QString& ClientName) + : AudioInterface(InputChans, OutputChans, MIX_UNSET, #ifdef WAIR // wair NumNetRevChans, #endif // endwhere - AudioBitResolution) - , mNumInChans(NumInChans) - , mNumOutChans(NumOutChans) -#ifdef WAIR // WAIR - , mNumNetRevChans(NumNetRevChans) -#endif // endwhere - , mClient(NULL) - , mClientName(ClientName) - , mBroadcast(false) - , mJackTrip(jacktrip) -{ -} - -//******************************************************************************* -JackAudioInterface::JackAudioInterface( - int NumInChans, int NumOutChans, -#ifdef WAIR // wair - int NumNetRevChans, -#endif // endwhere - AudioInterface::audioBitResolutionT AudioBitResolution, const QString& ClientName) - : AudioInterface(nullptr, NumInChans, NumOutChans, -#ifdef WAIR // wair - NumNetRevChans, -#endif // endwhere - AudioBitResolution, false) - , mNumInChans(NumInChans) - , mNumOutChans(NumOutChans) -#ifdef WAIR // WAIR - , mNumNetRevChans(NumNetRevChans) -#endif // endwhere + AudioBitResolution, processWithNetwork, jacktrip) , mClient(NULL) , mClientName(ClientName) , mBroadcast(false) - , mJackTrip(nullptr) { - JackAudioInterface(nullptr, NumInChans, NumOutChans, -#ifdef WAIR // wair - NumNetRevChans, -#endif // endwhere - AudioBitResolution, ClientName); } //******************************************************************************* diff --git a/src/JackAudioInterface.h b/src/JackAudioInterface.h index 7a5501a..366cebc 100644 --- a/src/JackAudioInterface.h +++ b/src/JackAudioInterface.h @@ -39,7 +39,7 @@ #define __JACKAUDIOINTERFACE_H__ #include -//#include //for shared_ptr +// #include //for shared_ptr #ifdef USE_WEAK_JACK #include "weak_libjack.h" #else @@ -67,74 +67,76 @@ class JackAudioInterface : public AudioInterface { public: /** \brief The class constructor - * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) * \param NumInChans Number of Input Channels * \param NumOutChans Number of Output Channels * \param AudioBitResolution Audio Sample Resolutions in bits + * \param processWithNetwork Send audio to and from the network + * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) * \param ClientName Client name in Jack */ JackAudioInterface( - JackTrip* jacktrip, int NumInChans, int NumOutChans, -#ifdef WAIR // wair - int NumNetRevChans, -#endif // endwhere - AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16, - const QString& ClientName = QStringLiteral("JackTrip")); - /// \brief Overloaded class constructor with null JackTrip pointer - JackAudioInterface( - int NumInChans, int NumOutChans, + QVarLengthArray InputChans, QVarLengthArray OutputChans, #ifdef WAIR // wair int NumNetRevChans, #endif // endwhere AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16, + bool processWithNetwork = false, JackTrip* jacktrip = nullptr, const QString& ClientName = QStringLiteral("JackTrip")); /// \brief The class destructor virtual ~JackAudioInterface(); /// \brief Setup the client - virtual void setup(bool verbose = true); + virtual void setup(bool verbose = true) override; /** \brief Tell the JACK server that we are ready to roll. The * process-callback will start running. This runs on its own thread. * \return 0 on success, otherwise a non-zero error code */ - virtual int startProcess(); + virtual int startProcess() override; /** \brief Stops the process-callback thread * \return 0 on success, otherwise a non-zero error code */ - virtual int stopProcess(); + virtual int stopProcess() override; /// \brief Connect the default ports, capture to sends, and receives to playback - void connectDefaultPorts(); + void connectDefaultPorts() override; + + /// \brief Get Number of Input Channels + virtual int getNumInputChannels() const override { return mNumInChans; } + /// \brief Get Number of Output Channels + virtual int getNumOutputChannels() const override { return mNumOutChans; } //--------------SETTERS--------------------------------------------- /// \brief Set Client Name to something different that the default (JackTrip) - virtual void setClientName(const QString& ClientName) { mClientName = ClientName; } - virtual void setSampleRate(uint32_t /*sample_rate*/) + virtual void setClientName(const QString& ClientName) override + { + mClientName = ClientName; + } + virtual void setSampleRate(uint32_t /*sample_rate*/) override { std::cout << "WARNING: Setting the Sample Rate in Jack mode has no effect." << std::endl; } - virtual void setBufferSizeInSamples(uint32_t /*buf_size*/) + virtual void setBufferSizeInSamples(uint32_t /*buf_size*/) override { std::cout << "WARNING: Setting the Sample Rate in Jack mode has no effect." << std::endl; } - virtual void enableBroadcastOutput() { mBroadcast = true; } + virtual void enableBroadcastOutput() override { mBroadcast = true; } //------------------------------------------------------------------ //--------------GETTERS--------------------------------------------- /// \brief Get the actual client name assigned by the Jack server virtual QString getAssignedClientName() final { return mAssignedClientName; } /// \brief Get the Jack Server Sampling Rate, in samples/second - virtual uint32_t getSampleRate() const; + virtual uint32_t getSampleRate() const override; /// \brief Get the Jack Server Buffer Size, in samples - virtual uint32_t getBufferSizeInSamples() const; + virtual uint32_t getBufferSizeInSamples() const override; /// \brief Get the Jack Server Buffer Size, in bytes virtual uint32_t getBufferSizeInBytes() const { return (getBufferSizeInSamples() * getAudioBitResolution() / 8); } /// \brief Get size of each audio per channel, in bytes - virtual size_t getSizeInBytesPerChannel() const; + virtual size_t getSizeInBytesPerChannel() const override; //------------------------------------------------------------------ private: @@ -181,12 +183,7 @@ class JackAudioInterface : public AudioInterface // reference : http://article.gmane.org/gmane.comp.audio.jackit/12873 static int wrapperProcessCallback(jack_nframes_t nframes, void* arg); - int mNumInChans; ///< Number of Input Channels - int mNumOutChans; ///< Number of Output Channels -#ifdef WAIR // WAIR - int mNumNetRevChans; ///< Number of Network Audio Channels (network comb filters -#endif // endwhere - int mNumFrames; ///< Buffer block size, in samples + int mNumFrames; ///< Buffer block size, in samples jack_client_t* mClient; ///< Jack Client QString mClientName; ///< Jack Client Name @@ -201,9 +198,7 @@ class JackAudioInterface : public AudioInterface QVarLengthArray mBroadcastBuffer; ///< Vector of Output buffer/channel to write to JACK bool mBroadcast; - QVector mProcessPlugins; ///< Vector of ProcesPlugins static QMutex sJackMutex; ///< Mutex to make thread safe jack functions that are not - JackTrip* mJackTrip; ///< JackTrip Mediator Class pointer }; #endif diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index c829b7e..8732c6c 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -81,7 +82,8 @@ bool JackTrip::sJackStopped = false; //******************************************************************************* JackTrip::JackTrip(jacktripModeT JacktripMode, dataProtocolT DataProtocolType, - int NumChansIn, int NumChansOut, + int BaseChanIn, int NumChansIn, int BaseChanOut, int NumChansOut, + AudioInterface::inputMixModeT InputMixMode, #ifdef WAIR // WAIR int NumNetRevChans, #endif // endwhere @@ -96,8 +98,11 @@ JackTrip::JackTrip(jacktripModeT JacktripMode, dataProtocolT DataProtocolType, , mDataProtocol(DataProtocolType) , mPacketHeaderType(PacketHeaderType) , mAudiointerfaceMode(JackTrip::JACK) + , mBaseAudioChanIn(BaseChanIn) , mNumAudioChansIn(NumChansIn) + , mBaseAudioChanOut(BaseChanOut) , mNumAudioChansOut(NumChansOut) + , mInputMixMode(InputMixMode) #ifdef WAIR // WAIR , mNumNetRevChans(NumNetRevChans) #endif // endwhere @@ -188,12 +193,21 @@ void JackTrip::setupAudio( if (gVerboseFlag) std::cout << " JackTrip:setupAudio before new JackAudioInterface" << std::endl; - mAudioInterface = - new JackAudioInterface(this, mNumAudioChansIn, mNumAudioChansOut, + QVarLengthArray inputChannels; + QVarLengthArray outputChannels; + inputChannels.resize(mNumAudioChansIn); + outputChannels.resize(mNumAudioChansOut); + for (int i = 0; i < mNumAudioChansIn; i++) { + inputChannels[i] = 1 + i; + } + for (int i = 0; i < mNumAudioChansOut; i++) { + outputChannels[i] = 1 + i; + } + mAudioInterface = new JackAudioInterface(inputChannels, outputChannels, #ifdef WAIR // wair - mNumNetRevChans, + mNumNetRevChans, #endif // endwhere - mAudioBitResolution); + mAudioBitResolution, true, this); #ifdef WAIRTOHUB // WAIR @@ -236,8 +250,19 @@ void JackTrip::setupAudio( #ifdef NO_JACK /// \todo FIX THIS REPETITION OF CODE #ifdef RT_AUDIO cout << "Warning: using non jack version, RtAudio will be used instead" << endl; - mAudioInterface = new RtAudioInterface(this, mNumAudioChansIn, mNumAudioChansOut, - mAudioBitResolution); + QVarLengthArray inputChannels; + QVarLengthArray outputChannels; + inputChannels.resize(mNumAudioChansIn); + outputChannels.resize(mNumAudioChansOut); + for (int i = 0; i < mNumAudioChansIn; i++) { + inputChannels[i] = mBaseAudioChanIn + i; + } + for (int i = 0; i < mNumAudioChansOut; i++) { + outputChannels[i] = mBaseAudioChanOut + i; + } + mAudioInterface = + new RtAudioInterface(inputChannels, outputChannels, mInputMixMode, + mAudioBitResolution, true, this); mAudioInterface->setSampleRate(mSampleRate); mAudioInterface->setDeviceID(mDeviceID); mAudioInterface->setInputDevice(mInputDeviceName); @@ -245,16 +270,32 @@ void JackTrip::setupAudio( mAudioInterface->setBufferSizeInSamples(mAudioBufferSize); mAudioInterface->setup(true); // Setup might have reduced number of channels + + // TODO: Add check for if base input channel needs to change mNumAudioChansIn = mAudioInterface->getNumInputChannels(); mNumAudioChansOut = mAudioInterface->getNumOutputChannels(); + if (mNumAudioChansIn == 2 && mInputMixMode == AudioInterface::MIXTOMONO) { + mNumAudioChansIn = 1; + } // Setup might have changed buffer size mAudioBufferSize = mAudioInterface->getBufferSizeInSamples(); #endif #endif } else if (mAudiointerfaceMode == JackTrip::RTAUDIO) { #ifdef RT_AUDIO - mAudioInterface = new RtAudioInterface(this, mNumAudioChansIn, mNumAudioChansOut, - mAudioBitResolution); + QVarLengthArray inputChannels; + QVarLengthArray outputChannels; + inputChannels.resize(mNumAudioChansIn); + outputChannels.resize(mNumAudioChansOut); + for (int i = 0; i < mNumAudioChansIn; i++) { + inputChannels[i] = mBaseAudioChanIn + i; + } + for (int i = 0; i < mNumAudioChansOut; i++) { + outputChannels[i] = mBaseAudioChanOut + i; + } + mAudioInterface = + new RtAudioInterface(inputChannels, outputChannels, mInputMixMode, + mAudioBitResolution, true, this); mAudioInterface->setSampleRate(mSampleRate); mAudioInterface->setDeviceID(mDeviceID); mAudioInterface->setInputDevice(mInputDeviceName); @@ -262,8 +303,13 @@ void JackTrip::setupAudio( mAudioInterface->setBufferSizeInSamples(mAudioBufferSize); mAudioInterface->setup(true); // Setup might have reduced number of channels + + // TODO: Add check for if base input channel needs to change mNumAudioChansIn = mAudioInterface->getNumInputChannels(); mNumAudioChansOut = mAudioInterface->getNumOutputChannels(); + if (mNumAudioChansIn == 2 && mInputMixMode == AudioInterface::MIXTOMONO) { + mNumAudioChansIn = 1; + } // Setup might have changed buffer size mAudioBufferSize = mAudioInterface->getBufferSizeInSamples(); #endif @@ -466,11 +512,23 @@ void JackTrip::startProcess( // qDebug() << "before mJackTrip->startProcess" << mReceiverBindPort<< // mSenderBindPort; #endif - checkIfPortIsBinded(mReceiverBindPort); + if (checkIfPortIsBinded(mReceiverBindPort)) { + stop(QStringLiteral("Could not bind %1 UDP socket. It may already be binded by " + "another process on " + "your machine. Try using a different port number") + .arg(mReceiverBindPort)); + return; + } if (gVerboseFlag) std::cout << " JackTrip:startProcess before checkIfPortIsBinded(mSenderBindPort)" << std::endl; - checkIfPortIsBinded(mSenderBindPort); + if (checkIfPortIsBinded(mSenderBindPort)) { + stop(QStringLiteral("Could not bind %1 UDP socket. It may already be binded by " + "another process on " + "your machine. Try using a different port number") + .arg(mSenderBindPort)); + return; + } // Set all classes and parameters // ------------------------------ if (gVerboseFlag) @@ -1290,7 +1348,7 @@ int JackTrip::clientPingToServerStart() connect(&mTcpClient, &QTcpSocket::readyRead, this, &JackTrip::receivedDataTCP); connect(&mTcpClient, &QTcpSocket::connected, this, &JackTrip::receivedConnectionTCP); // Enable CI builds on Ubuntu 20.04 with Qt 5.12.8 -#ifdef __linux__ +#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) connect(&mTcpClient, QOverload::of(&QAbstractSocket::error), this, &JackTrip::receivedErrorTCP); @@ -1303,7 +1361,8 @@ int JackTrip::clientPingToServerStart() mAwaitingTcp = true; mElapsedTime = 0; mEndTime = 30000; // Timeout after 30 seconds. - mRetryTimer.setInterval(randomizer.bounded(0, 2000 * pow(2, mRetries))); + mRetryTimer.setInterval(randomizer.bounded( + static_cast(0), static_cast(2000 * pow(2, mRetries)))); mRetryTimer.setSingleShot(true); mRetryTimer.disconnect(); connect(&mRetryTimer, &QTimer::timeout, this, &JackTrip::tcpTimerTick); @@ -1495,17 +1554,27 @@ bool JackTrip::checkPeerSettings(int8_t* full_packet) } //******************************************************************************* -void JackTrip::checkIfPortIsBinded(int port) +bool JackTrip::checkIfPortIsBinded(int port) { QUdpSocket UdpSockTemp; // Create socket to wait for client // Bind the socket // cc if ( !UdpSockTemp.bind(QHostAddress::AnyIPv4, port, // QUdpSocket::DontShareAddress) ) - if (!UdpSockTemp.bind(QHostAddress::Any, port, QUdpSocket::DontShareAddress)) { - UdpSockTemp.close(); // close the socket - throw std::runtime_error( - "Could not bind UDP socket. It may already be binded by another process on " - "your machine. Try using a different port number"); + + // check all combinations to ensure the port is free + std::map interfaces = { + {"IPv4", QHostAddress::AnyIPv4}, + {"IPv6", QHostAddress::AnyIPv6}, + {"IPv4+IPv6", QHostAddress::Any}}; + + std::map::iterator it; + for (it = interfaces.begin(); it != interfaces.end(); it++) { + bool binded = UdpSockTemp.bind(it->second, port, QUdpSocket::DontShareAddress); + if (!binded) { + UdpSockTemp.close(); // close the socket + return true; + } + UdpSockTemp.close(); } - UdpSockTemp.close(); // close the socket + return false; } diff --git a/src/JackTrip.h b/src/JackTrip.h index c86151a..932f32d 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -134,7 +134,9 @@ class JackTrip : public QObject */ JackTrip( jacktripModeT JacktripMode = CLIENT, dataProtocolT DataProtocolType = UDP, - int NumChansIn = gDefaultNumInChannels, int NumChansOut = gDefaultNumInChannels, + int BaseChanIn = 0, int NumChansIn = gDefaultNumInChannels, int BaseChanOut = 0, + int NumChansOut = gDefaultNumInChannels, + AudioInterface::inputMixModeT InputMixMode = AudioInterface::MIX_UNSET, #ifdef WAIR // wair int NumNetRevChans = 0, #endif // endwhere @@ -191,7 +193,7 @@ class JackTrip : public QObject /// \brief Check if UDP port is already binded /// \param port Port number - virtual void checkIfPortIsBinded(int port); + virtual bool checkIfPortIsBinded(int port); //------------------------------------------------------------------------------------ /// \name Getters and Setters Methods to change parameters after construction @@ -624,8 +626,11 @@ class JackTrip : public QObject DataProtocol::packetHeaderTypeT mPacketHeaderType; ///< Packet Header Type JackTrip::audiointerfaceModeT mAudiointerfaceMode; - int mNumAudioChansIn; ///< Number of Audio Input Channels - int mNumAudioChansOut; ///< Number of Audio Output Channels + int mBaseAudioChanIn; ///< Base Audio Input Channel + int mNumAudioChansIn; ///< Number of Audio Input Channels + int mBaseAudioChanOut; ///< Base Audio Output Channel + int mNumAudioChansOut; ///< Number of Audio Output Channels + AudioInterface::inputMixModeT mInputMixMode; ///< Input mix mode #ifdef WAIR // WAIR int mNumNetRevChans; ///< Number of Network Audio Channels (net comb filters) diff --git a/src/JackTripWorker.cpp b/src/JackTripWorker.cpp index 2165490..0f6a3d5 100644 --- a/src/JackTripWorker.cpp +++ b/src/JackTripWorker.cpp @@ -125,8 +125,9 @@ void JackTripWorker::setJackTrip(int id, const QString& client_address, // qDebug() << "is WAIR?" << tmp ; qDebug() << "mNumNetRevChans" << mNumNetRevChans; - mJackTrip.reset(new JackTrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, 1, 1, - mNumNetRevChans, FORCEBUFFERQ)); + mJackTrip.reset(new JackTrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, 0, 1, 0, 1, + AudioInterface::MIX_UNSET, mNumNetRevChans, + FORCEBUFFERQ)); // Add Plugins if (mWAIR) { cout << "Running in WAIR Mode..." << endl; @@ -146,8 +147,8 @@ void JackTripWorker::setJackTrip(int id, const QString& client_address, } } #else // endwhere - mJackTrip.reset(new JackTrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, 1, 1, - mBufferQueueLength)); + mJackTrip.reset(new JackTrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, 0, 1, 0, 1, + AudioInterface::MIX_UNSET, mBufferQueueLength)); #endif // not wair #endif // ifndef __JAMTEST__ diff --git a/src/Meter.cpp b/src/Meter.cpp index 14b26ed..031e284 100644 --- a/src/Meter.cpp +++ b/src/Meter.cpp @@ -38,8 +38,6 @@ #include "Meter.h" -#include - #include "jacktrip_types.h" //******************************************************************************* @@ -57,11 +55,7 @@ void Meter::init(int samplingRate) } /* Set meter values to the default floor */ - mValues.resize(mNumChannels); - QVector::iterator it; - for (it = mValues.begin(); it != mValues.end(); ++it) { - *it = threshold; - } + setupValues(); /* Start timer */ int timeout_ms = 100; @@ -93,21 +87,25 @@ void Meter::compute(int nframes, float** inputs, float** /*_*/) init(fSamplingFreq); } - QVector meterBuf(nframes); - float* meterBufPtr = meterBuf.data(); - float** output = &meterBufPtr; + if (mBufSize < nframes) { + if (mBuffer) { + delete mBuffer; + } + mBufSize = nframes; + mBuffer = new float[mBufSize]; + } + for (int i = 0; i < mNumChannels; i++) { /* Run the signal through Faust */ - meterP[i]->compute(nframes, &inputs[i], output); + meterP[i]->compute(nframes, &inputs[i], &mBuffer); /* Use the existing value of mValues[i] as the threshold - this will be reset to the default floor of -80dB on each timeout */ float max = mValues[i]; - QVector::iterator it; - for (it = meterBuf.begin(); it != meterBuf.end(); ++it) { - if (*it > max) { - max = *it; + for (int j = 0; j < nframes; j++) { + if (mBuffer[j] > max) { + max = mBuffer[j]; } } @@ -133,26 +131,43 @@ void Meter::updateNumChannels(int nChansIn, int nChansOut) mNumChannels = nChansOut; } - mValues.resize(mNumChannels); - QVector::iterator it; - for (it = mValues.begin(); it != mValues.end(); ++it) { - *it = threshold; + setupValues(); +} + +void Meter::setupValues() +{ + if (mValues) { + float* oldValues = mValues; + // Delete our old array after 5 seconds. + QTimer::singleShot(5000, this, [=]() { + delete oldValues; + }); + } + if (mOutValues) { + float* oldOut = mOutValues; + QTimer::singleShot(5000, this, [=]() { + delete oldOut; + }); + } + mValues = new float[mNumChannels]; + mOutValues = new float[mNumChannels]; + for (int i = 0; i < mNumChannels; i++) { + mValues[i] = threshold; + mOutValues[i] = threshold; } } //******************************************************************************* void Meter::onTick() { + /* Set meter values to the default floor */ + for (int i = 0; i < mNumChannels; i++) { + mOutValues[i] = mValues[i]; + mValues[i] = threshold; + } + if (hasProcessedAudio) { /* Send the measurements to whatever other component requests it */ - QVector valuesCopy(mValues); - valuesCopy.detach(); - emit onComputedVolumeMeasurements(valuesCopy); - - /* Set meter values to the default floor */ - QVector::iterator it; - for (it = mValues.begin(); it != mValues.end(); ++it) { - *it = threshold; - } + emit onComputedVolumeMeasurements(mOutValues, mNumChannels); } -} \ No newline at end of file +} diff --git a/src/Meter.h b/src/Meter.h index a475dc0..fa4269b 100644 --- a/src/Meter.h +++ b/src/Meter.h @@ -41,7 +41,6 @@ #include #include -#include #include #include @@ -73,6 +72,15 @@ class Meter : public ProcessPlugin delete meterP[i]; } meterP.clear(); + if (mValues) { + delete mValues; + } + if (mOutValues) { + delete mOutValues; + } + if (mBuffer) { + delete mBuffer; + } } void init(int samplingRate) override; @@ -84,6 +92,8 @@ class Meter : public ProcessPlugin void updateNumChannels(int nChansIn, int nChansOut) override; private: + void setupValues(); + float fs; int mNumChannels; float threshold = -80.0; @@ -91,13 +101,16 @@ class Meter : public ProcessPlugin bool hasProcessedAudio = false; QTimer mTimer; - QVector mValues; + float* mValues = nullptr; + float* mOutValues = nullptr; + float* mBuffer = nullptr; + int mBufSize = 0; private slots: void onTick(); signals: - void onComputedVolumeMeasurements(QVector values); + void onComputedVolumeMeasurements(float* values, int n); }; -#endif \ No newline at end of file +#endif diff --git a/src/Regulator.cpp b/src/Regulator.cpp index ea6dc2c..5b88bcd 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -281,7 +281,8 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) { if (seq_num != -1) { if (!mFPPratioIsSet) { // first peer packet - mPeerFPP = len / (mNumChannels * mBitResolutionMode); + mPeerFPP = len / (mNumChannels * mBitResolutionMode); + mPeerFPPdurMsec = 1000.0 * mPeerFPP / 48000.0; // bufstrategy 1 autoq mode overloads qLen with negative val // creates this ugly code if (mMsecTolerance < 0) { // handle -q auto or, for example, -q auto10 @@ -338,7 +339,8 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num) } } pushStat->tick(); - double adjustAuto = pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec); + double adjustAuto = + pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec, mPeerFPPdurMsec); // qDebug() << adjustAuto; if (mAuto && (pushStat->lastTime > AutoInitDur)) mMsecTolerance = adjustAuto; @@ -664,6 +666,9 @@ void BurgAlgorithm::predict(std::vector& coeffs, std::vector //******************************************************************************* ChanData::ChanData(int i, int FPP, int hist) : ch(i) { + int shrinkCoeffsFactor = 1; + if (FPP == 1024) + shrinkCoeffsFactor = 8; trainSamps = (hist * FPP); mTruth.resize(FPP, 0.0); mXfadedPred.resize(FPP, 0.0); @@ -674,7 +679,7 @@ ChanData::ChanData(int i, int FPP, int hist) : ch(i) } mTrain.resize(trainSamps, 0.0); mPrediction.resize(trainSamps - 1, 0.0); // ORDER - mCoeffs.resize(trainSamps - 2, 0.0); + mCoeffs.resize(trainSamps / shrinkCoeffsFactor - 2, 0.0); mCrossFadeDown.resize(FPP, 0.0); mCrossFadeUp.resize(FPP, 0.0); mCrossfade.resize(FPP, 0.0); @@ -709,7 +714,7 @@ void StdDev::reset() max = -999999.0; }; -double StdDev::calcAuto(double autoHeadroom, double localFPPdur) +double StdDev::calcAuto(double autoHeadroom, double localFPPdur, double peerFPPdur) { // qDebug() << longTermStdDev << longTermMax << AutoMax << window << // longTermCnt; @@ -717,7 +722,9 @@ double StdDev::calcAuto(double autoHeadroom, double localFPPdur) return AutoMax; double tmp = longTermStdDev + ((longTermMax > AutoMax) ? AutoMax : longTermMax); if (tmp < localFPPdur) - tmp = localFPPdur; // might also check peerFPP... + tmp = localFPPdur; + if (tmp < peerFPPdur) + tmp = peerFPPdur; tmp += autoHeadroom; return tmp; }; diff --git a/src/Regulator.h b/src/Regulator.h index cfe631e..1cad71b 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -90,7 +90,7 @@ class StdDev public: StdDev(int id, QElapsedTimer* timer, int w); void tick(); - double calcAuto(double autoHeadroom, double localFPPdur); + double calcAuto(double autoHeadroom, double localFPPdur, double peerFPPdur); int mId; int plcOverruns; int plcUnderruns; @@ -207,6 +207,7 @@ class Regulator : public RingBuffer int mModSeqNumPeer; double mAutoHeadroom; double mFPPdurMsec; + double mPeerFPPdurMsec; void changeGlobal(double); void changeGlobal_2(int); void changeGlobal_3(int); diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index b061fdd..9be6c60 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -41,44 +41,71 @@ #include #include "JackTrip.h" +#include "StereoToMono.h" #include "jacktrip_globals.h" using std::cout; using std::endl; //******************************************************************************* -RtAudioInterface::RtAudioInterface(JackTrip* jacktrip, int NumInChans, int NumOutChans, - audioBitResolutionT AudioBitResolution) - : AudioInterface(jacktrip, NumInChans, NumOutChans, AudioBitResolution) +RtAudioInterface::RtAudioInterface(QVarLengthArray InputChans, + QVarLengthArray OutputChans, + inputMixModeT InputMixMode, + audioBitResolutionT AudioBitResolution, + bool processWithNetwork, JackTrip* jacktrip) + : AudioInterface(InputChans, OutputChans, InputMixMode, AudioBitResolution, + processWithNetwork, jacktrip) , mRtAudio(NULL) { } -//******************************************************************************* -RtAudioInterface::RtAudioInterface(int NumInChans, int NumOutChans, - audioBitResolutionT AudioBitResolution) - : AudioInterface(nullptr, NumInChans, NumOutChans, AudioBitResolution, false) - , mRtAudio(NULL) -{ - RtAudioInterface(nullptr, NumInChans, NumOutChans, AudioBitResolution); -} - //******************************************************************************* RtAudioInterface::~RtAudioInterface() { if (mRtAudio != NULL) { delete mRtAudio; } + + if (mStereoToMonoMixer != NULL) { + delete mStereoToMonoMixer; + } } //******************************************************************************* void RtAudioInterface::setup(bool verbose) { // Initialize Buffer array to read and write audio and members - mNumInChans = getNumInputChannels(); - mNumOutChans = getNumOutputChannels(); - mInBuffer.resize(getNumInputChannels()); - mOutBuffer.resize(getNumOutputChannels()); + QVarLengthArray iChans = getInputChannels(); + QVarLengthArray oChans = getOutputChannels(); + + uint32_t chansIn = iChans.size(); + uint32_t chansOut = oChans.size(); + uint32_t baseChanIn = 0; + uint32_t baseChanOut = 0; + + if (iChans.size() >= 1) { + int min = iChans.at(0); + for (int i = 0; i < iChans.size(); i++) { + if (iChans.at(i) < min) { + min = iChans.at(i); + } + } + if (min >= 0) { + baseChanIn = min; + } + } + + if (oChans.size() >= 1) { + int min = oChans.at(0); + for (int i = 0; i < oChans.size(); i++) { + if (oChans.at(i) < min) { + min = iChans.at(i); + } + } + if (min >= 0) { + baseChanOut = min; + } + } cout << "Setting Up RtAudio Interface" << endl; cout << gPrintSeparator << endl; @@ -93,8 +120,8 @@ void RtAudioInterface::setup(bool verbose) QStringList all_input_devices; QStringList all_output_devices; - getDeviceList(&all_input_devices, NULL, true); - getDeviceList(&all_output_devices, NULL, false); + getDeviceList(&all_input_devices, NULL, NULL, true); + getDeviceList(&all_output_devices, NULL, NULL, false); unsigned int n_devices_input = all_input_devices.size(); unsigned int n_devices_output = all_output_devices.size(); @@ -170,12 +197,20 @@ void RtAudioInterface::setup(bool verbose) auto dev_info_input = rtAudioIn->getDeviceInfo(index_in); auto dev_info_output = rtAudioOut->getDeviceInfo(index_out); - if (static_cast(getNumInputChannels()) > dev_info_input.inputChannels) { - setNumInputChannels(dev_info_input.inputChannels); + if (baseChanIn + chansIn > dev_info_input.inputChannels) { + baseChanIn = 0; + chansIn = 2; + if (dev_info_input.inputChannels < 2) { + chansIn = 1; + } } - if (static_cast(getNumOutputChannels()) - > dev_info_output.outputChannels) { - setNumOutputChannels(dev_info_output.outputChannels); + + if (baseChanOut + chansOut > dev_info_output.outputChannels) { + baseChanOut = 0; + chansOut = 2; + if (dev_info_output.outputChannels < 2) { + chansOut = 1; + } } if (verbose) { @@ -212,10 +247,12 @@ void RtAudioInterface::setup(bool verbose) } RtAudio::StreamParameters in_params, out_params; - in_params.deviceId = index_in; - out_params.deviceId = index_out; - in_params.nChannels = getNumInputChannels(); - out_params.nChannels = getNumOutputChannels(); + in_params.deviceId = index_in; + out_params.deviceId = index_out; + in_params.nChannels = chansIn; + out_params.nChannels = chansOut; + in_params.firstChannel = baseChanIn; + out_params.firstChannel = baseChanOut; RtAudio::StreamOptions options; // The second flag affects linux and mac only @@ -227,8 +264,31 @@ void RtAudioInterface::setup(bool verbose) options.priority = 30; options.streamName = gJackDefaultClientName; + // Update parent class + QVarLengthArray updatedInputChannels; + QVarLengthArray updatedOutputChannels; + updatedInputChannels.resize(chansIn); + updatedOutputChannels.resize(chansOut); + for (uint32_t i = 0; i < chansIn; i++) { + updatedInputChannels[i] = baseChanIn + i; + } + for (uint32_t i = 0; i < chansOut; i++) { + updatedOutputChannels[i] = baseChanOut + i; + } + setInputChannels(updatedInputChannels); + setOutputChannels(updatedOutputChannels); + + // Setup buffers + mInBuffer.resize(chansIn); + mOutBuffer.resize(chansOut); + unsigned int sampleRate = getSampleRate(); // mSamplingRate; unsigned int bufferFrames = getBufferSizeInSamples(); // mBufferSize; + mStereoToMonoMixer = new StereoToMono(); + mStereoToMonoMixer->init(sampleRate); + + // Setup parent class + AudioInterface::setup(verbose); try { // IMPORTANT NOTE: It's VERY important to remember to pass "this" @@ -245,9 +305,6 @@ void RtAudioInterface::setup(bool verbose) std::cout << e.getMessage() << '\n' << std::endl; throw std::runtime_error(e.getMessage()); } - - // Setup parent class - AudioInterface::setup(verbose); } //******************************************************************************* @@ -343,18 +400,23 @@ int RtAudioInterface::RtAudioCallback(void* outputBuffer, void* inputBuffer, inputBuffer_sample = (sample_t*)inputBuffer; outputBuffer_sample = (sample_t*)outputBuffer; + int chansIn = getNumInputChannels(); if (inputBuffer_sample != NULL && outputBuffer_sample != NULL) { // Get input and output buffers //------------------------------------------------------------------- - for (int i = 0; i < mNumInChans; i++) { + for (int i = 0; i < mInBuffer.size(); i++) { // Input Ports are READ ONLY mInBuffer[i] = inputBuffer_sample + (nFrames * i); } - for (int i = 0; i < mNumOutChans; i++) { + + for (int i = 0; i < mOutBuffer.size(); i++) { // Output Ports are WRITABLE mOutBuffer[i] = outputBuffer_sample + (nFrames * i); } - + if (chansIn == 2 && mInBuffer.size() == chansIn + && mInputMixMode == AudioInterface::MIXTOMONO) { + mStereoToMonoMixer->compute(nFrames, mInBuffer.data(), mInBuffer.data()); + } AudioInterface::callback(mInBuffer, mOutBuffer, nFrames); } @@ -417,38 +479,22 @@ int RtAudioInterface::stopProcess() //******************************************************************************* void RtAudioInterface::getDeviceList(QStringList* list, QStringList* categories, - bool isInput) + QList* channels, bool isInput) { RtAudio baseRtAudio; RtAudio::Api baseRtAudioApi = baseRtAudio.getCurrentApi(); - - // Add (default) - list->clear(); - list->append(QStringLiteral("(default)")); if (categories != NULL) { -#ifdef _WIN32 - switch (baseRtAudioApi) { - case RtAudio::WINDOWS_ASIO: - categories->append(QStringLiteral("Low-Latency (ASIO)")); - break; - case RtAudio::WINDOWS_WASAPI: - categories->append(QStringLiteral("All Devices (Non-ASIO)")); - break; - case RtAudio::WINDOWS_DS: - categories->append(QStringLiteral("All Devices (Non-ASIO)")); - break; - default: - categories->append(QStringLiteral("")); - break; - } -#else - categories->append(QStringLiteral("")); -#endif + categories->clear(); + } + if (channels != NULL) { + channels->clear(); } + list->clear(); // Explicitly add default device QString defaultDeviceName = ""; uint32_t defaultDeviceIdx; + RtAudio::DeviceInfo defaultDeviceInfo; if (isInput) { defaultDeviceIdx = baseRtAudio.getDefaultInputDevice(); } else { @@ -456,8 +502,8 @@ void RtAudioInterface::getDeviceList(QStringList* list, QStringList* categories, } if (defaultDeviceIdx != 0) { - RtAudio::DeviceInfo info = baseRtAudio.getDeviceInfo(defaultDeviceIdx); - defaultDeviceName = QString::fromStdString(info.name); + defaultDeviceInfo = baseRtAudio.getDeviceInfo(defaultDeviceIdx); + defaultDeviceName = QString::fromStdString(defaultDeviceInfo.name); } if (defaultDeviceName != "") { @@ -482,6 +528,13 @@ void RtAudioInterface::getDeviceList(QStringList* list, QStringList* categories, categories->append(QStringLiteral("")); #endif } + if (channels != NULL) { + if (isInput) { + channels->append(defaultDeviceInfo.inputChannels); + } else { + channels->append(defaultDeviceInfo.outputChannels); + } + } } std::vector apis; @@ -520,8 +573,16 @@ void RtAudioInterface::getDeviceList(QStringList* list, QStringList* categories, if (isInput && info.inputChannels > 0) { list->append(QString::fromStdString(info.name)); + if (channels != NULL) { + channels->append(info.inputChannels); + } } else if (!isInput && info.outputChannels > 0) { list->append(QString::fromStdString(info.name)); + if (channels != NULL) { + channels->append(info.outputChannels); + } + } else { + continue; } if (categories == NULL) { @@ -568,7 +629,8 @@ void RtAudioInterface::getDeviceInfoFromName(std::string deviceName, int* index, unsigned int devices = rtaudio.getDeviceCount(); for (unsigned int j = 0; j < devices; j++) { RtAudio::DeviceInfo info = rtaudio.getDeviceInfo(j); - if (info.probed == true && deviceName == info.name) { + if (info.probed == true + && deviceName == QString::fromStdString(info.name).toStdString()) { if ((isInput && info.inputChannels > 0) || (!isInput && info.outputChannels > 0)) { *index = j; @@ -582,4 +644,4 @@ void RtAudioInterface::getDeviceInfoFromName(std::string deviceName, int* index, *index = -1; *api = ""; return; -} \ No newline at end of file +} diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h index bb1b746..91f2bf5 100644 --- a/src/RtAudioInterface.h +++ b/src/RtAudioInterface.h @@ -43,6 +43,7 @@ #include #include "AudioInterface.h" +#include "StereoToMono.h" #include "jacktrip_globals.h" class JackTrip; // Forward declaration @@ -51,18 +52,16 @@ class RtAudioInterface : public AudioInterface { public: /** \brief The class constructor - * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) * \param NumInChans Number of Input Channels * \param NumOutChans Number of Output Channels * \param AudioBitResolution Audio Sample Resolutions in bits + * \param processWithNetwork Send audio to and from the network + * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator) */ - RtAudioInterface(JackTrip* jacktrip, int NumInChans = gDefaultNumInChannels, - int NumOutChans = gDefaultNumOutChannels, - audioBitResolutionT AudioBitResolution = BIT16); - /// \brief Overloaded class constructor with null JackTrip pointer - RtAudioInterface(int NumInChans = gDefaultNumInChannels, - int NumOutChans = gDefaultNumOutChannels, - audioBitResolutionT AudioBitResolution = BIT16); + RtAudioInterface(QVarLengthArray InputChans, QVarLengthArray OutputChans, + inputMixModeT InputMixMode = AudioInterface::MIX_UNSET, + audioBitResolutionT AudioBitResolution = BIT16, + bool processWithNetwork = false, JackTrip* jacktrip = nullptr); /// \brief The class destructor virtual ~RtAudioInterface(); @@ -74,7 +73,8 @@ class RtAudioInterface : public AudioInterface /// \brief This has no effect in RtAudio virtual void connectDefaultPorts() {} - static void getDeviceList(QStringList* list, QStringList* categories, bool isInput); + static void getDeviceList(QStringList* list, QStringList* categories, + QList* channels, bool isInput); static void getDeviceInfoFromName(std::string deviceName, int* index, std::string* api, bool isInput); @@ -96,14 +96,15 @@ class RtAudioInterface : public AudioInterface const std::string& errorText); void printDeviceInfo(std::string api, unsigned int deviceId); - int mNumInChans; ///< Number of Input Channels - int mNumOutChans; ///< Number of Output Channels QVarLengthArray mInBuffer; ///< Vector of Input buffers/channel read from JACK QVarLengthArray mOutBuffer; ///< Vector of Output buffer/channel to write to JACK RtAudio* mRtAudio; ///< RtAudio class if the input and output device are the same + unsigned int getDefaultDeviceForLinuxPulseAudio(bool isInput); + + StereoToMono* mStereoToMonoMixer = NULL; }; #endif // __RTAUDIOINTERFACE_H__ diff --git a/src/Settings.cpp b/src/Settings.cpp index 60ac520..21d119b 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -38,7 +38,7 @@ #include "Settings.h" #include "LoopBack.h" -//#include "NetKS.h" +// #include "NetKS.h" #include "Effects.h" #ifdef WAIR // wair @@ -46,7 +46,7 @@ #include "ap8x2.dsp.h" #endif // endwhere -//#include "JackTripWorker.h" +// #include "JackTripWorker.h" #include // for command line parsing #include @@ -73,7 +73,7 @@ #define PRINT_BUILD_INFO cout << "Build Info: " << TO_STRING(JACKTRIP_BUILD_INFO) << endl; #endif -//#include "ThreadPoolTest.h" +// #include "ThreadPoolTest.h" using std::cout; using std::endl; @@ -97,7 +97,9 @@ enum JTLongOptIDS { OPT_LISTDEVICES, OPT_AUDIODEVICE, OPT_AUDIOINPUTDEVICE, - OPT_AUDIOOUTPUTDEVICE + OPT_AUDIOOUTPUTDEVICE, + OPT_GUI, + OPT_DEEPLINK }; //******************************************************************************* @@ -106,7 +108,7 @@ void Settings::parseInput(int argc, char** argv) // Always use decimal point for floating point numbers setlocale(LC_NUMERIC, "C"); // If no command arguments are given, print instructions - if (argc == 1) { + if (argc == 1 && !mGuiEnabled) { printUsage(); std::exit(0); } @@ -153,10 +155,11 @@ void Settings::parseInput(int argc, char** argv) {"clientname", required_argument, NULL, 'J'}, // Run in JamLink mode {"remotename", required_argument, NULL, 'K'}, // Client name on hub server {"appendthreadid", no_argument, NULL, - OPT_APPENDTHREADID}, // Append thread id to client names + OPT_APPENDTHREADID}, // Append thread id to client names + {"srate", required_argument, NULL, 'T'}, // Set Sample Rate + {"bufsize", required_argument, NULL, 'F'}, // Set buffer Size #ifdef RT_AUDIO - {"rtaudio", no_argument, NULL, 'R'}, // Run in JamLink mode - {"srate", required_argument, NULL, 'T'}, // Set Sample Rate + {"rtaudio", no_argument, NULL, 'R'}, // Run in JamLink mode {"deviceid", required_argument, NULL, 'd'}, // Set RTAudio device id to use (DEPRECATED) {"audiodevice", required_argument, NULL, @@ -166,8 +169,7 @@ void Settings::parseInput(int argc, char** argv) {"audiooutputdevice", required_argument, NULL, OPT_AUDIOOUTPUTDEVICE}, // Set RTAudio output device by name {"listdevices", no_argument, NULL, - OPT_LISTDEVICES}, // Set RTAudio device id to use - {"bufsize", required_argument, NULL, 'F'}, // Set buffer Size + OPT_LISTDEVICES}, // Set RTAudio device id to use #endif {"nojackportsconnect", no_argument, NULL, 'D'}, // Don't connect default Audio Ports @@ -198,13 +200,15 @@ void Settings::parseInput(int argc, char** argv) OPT_AUTHKEY}, // Private key for server authentication {"credsfile", required_argument, NULL, OPT_AUTHCREDS}, // Username and password store for server authentication - {"username", required_argument, NULL, + {"username", optional_argument, NULL, OPT_AUTHUSER}, // Username when using authentication as a hub client - {"password", required_argument, NULL, + {"password", optional_argument, NULL, OPT_AUTHPASS}, // Password when using authentication as a hub client {"help", no_argument, NULL, 'h'}, // Print Help {"examine-audio-delay", required_argument, NULL, 'x'}, // test mode - measure audio round-trip latency statistics + {"gui", no_argument, NULL, OPT_GUI}, // Force GUI mode + {"deeplink", optional_argument, NULL, OPT_DEEPLINK}, // VirtualStudio Deeplink {NULL, 0, NULL, 0}}; // Parse Command Line Arguments @@ -213,9 +217,9 @@ void Settings::parseInput(int argc, char** argv) int ch; while ((ch = getopt_long( argc, argv, - "n:N:H:sc:SC:o:B:P:U:q:r:b:ztlwjeJ:K:RTd:F:p:uiDvVhI:G:f:O:a:x:A", + "n:N:H:sc:SC:L:o:B:P:U:q:r:b:ztlwjeJ:K:RT:d:F:p:uiDvVhI:G:f:O:a:x:A", longopts, NULL)) - != -1) + != -1) { switch (ch) { case OPT_NUMRECEIVE: if (0 < atoi(optarg)) { @@ -269,24 +273,29 @@ void Settings::parseInput(int argc, char** argv) case 's': // Run in P2P server mode //------------------------------------------------------- mJackTripMode = JackTrip::SERVER; + checkMode(); break; case 'S': // Run in Hub server mode //------------------------------------------------------- - mJackTripServer = true; + mJackTripMode = JackTrip::SERVERPINGSERVER; + checkMode(); break; case 'c': // P2P client mode //------------------------------------------------------- mJackTripMode = JackTrip::CLIENT; mPeerAddress = optarg; + checkMode(); break; case 'L': // set optional local host address //------------------------------------------------------- - mLocalAddress = optarg; + mGuiIgnoresArguments = true; + mLocalAddress = optarg; break; case 'C': // Ping to server //------------------------------------------------------- mJackTripMode = JackTrip::CLIENTTOPINGSERVER; mPeerAddress = optarg; + checkMode(); break; case 'o': // Port Offset //------------------------------------------------------- @@ -367,15 +376,18 @@ void Settings::parseInput(int argc, char** argv) break; case 'l': // loopback //------------------------------------------------------- - mLoopBack = true; + mGuiIgnoresArguments = true; + mLoopBack = true; break; case 'e': // jamlink //------------------------------------------------------- - mEmptyHeader = true; + mGuiIgnoresArguments = true; + mEmptyHeader = true; break; case 'j': // jamlink //------------------------------------------------------- - mJamLink = true; + mGuiIgnoresArguments = true; + mJamLink = true; break; case 'J': // Set client Name //------------------------------------------------------- @@ -388,43 +400,56 @@ void Settings::parseInput(int argc, char** argv) case OPT_APPENDTHREADID: mAppendThreadID = true; break; -#ifdef RT_AUDIO - case 'R': // RtAudio + case 'T': // Sampling Rate //------------------------------------------------------- - mUseJack = false; + mGuiIgnoresArguments = true; + mChangeDefaultSR = true; + mSampleRate = atoi(optarg); break; - case 'T': // Sampling Rate + case 'F': // Buffer Size //------------------------------------------------------- - mChangeDefaultSR = true; - mSampleRate = atoi(optarg); + mGuiIgnoresArguments = true; + mChangeDefaultBS = true; + mAudioBufferSize = atoi(optarg); + break; +#ifdef RT_AUDIO + case 'R': // RtAudio + //------------------------------------------------------- + mGuiIgnoresArguments = true; + mUseJack = false; break; case 'd': // RTAudio device id //------------------------------------------------------- + mGuiIgnoresArguments = true; cout << "WARNING: Setting device ID is deprecated and will be removed in the " "future." << endl; mChangeDefaultID = true; mDeviceID = atoi(optarg); break; - case 'F': // Buffer Size - //------------------------------------------------------- - mChangeDefaultBS = true; - mAudioBufferSize = atoi(optarg); - break; case OPT_AUDIODEVICE: // Set audio device //------------------------------------------------------- - setDevicesByString(optarg); + mGuiIgnoresArguments = true; + if (!mGuiEnabled) { + // Don't try to parse this if we're in the GUI and ignoring it. + setDevicesByString(optarg); + } break; case OPT_AUDIOINPUTDEVICE: - mInputDeviceName = optarg; + mGuiIgnoresArguments = true; + mInputDeviceName = optarg; break; case OPT_AUDIOOUTPUTDEVICE: - mOutputDeviceName = optarg; + mGuiIgnoresArguments = true; + mOutputDeviceName = optarg; break; case OPT_LISTDEVICES: // List audio devices //------------------------------------------------------- - RtAudioInterface::printDevices(); - std::exit(0); + mGuiIgnoresArguments = true; + if (!mGuiEnabled) { + RtAudioInterface::printDevices(); + std::exit(0); + } break; #endif case 'D': @@ -484,8 +509,9 @@ void Settings::parseInput(int argc, char** argv) break; case 'I': // IO Stat timeout //------------------------------------------------------- - mIOStatTimeout = atoi(optarg); - if (0 > mIOStatTimeout) { + mGuiIgnoresArguments = true; + mIOStatTimeout = atoi(optarg); + if (0 > mIOStatTimeout && !mGuiEnabled) { printUsage(); std::cerr << "--iostat ERROR: negative timeout." << endl; std::exit(1); @@ -494,6 +520,10 @@ void Settings::parseInput(int argc, char** argv) case 'G': // IO Stat log file //------------------------------------------------------- { + mGuiIgnoresArguments = true; + if (mGuiEnabled) { + break; + } std::ofstream* outStream = new std::ofstream(optarg); if (!outStream->is_open()) { printUsage(); @@ -513,9 +543,11 @@ void Settings::parseInput(int argc, char** argv) } break; case OPT_SIMLOSS: // Simulate packet loss - mSimulatedLossRate = atof(optarg); + mGuiIgnoresArguments = true; + mSimulatedLossRate = atof(optarg); break; case OPT_SIMJITTER: // Simulate jitter + mGuiIgnoresArguments = true; char* endp; mSimulatedJitterRate = strtod(optarg, &endp); if (0 == *endp) { @@ -537,6 +569,10 @@ void Settings::parseInput(int argc, char** argv) break; case 'O': { // Overflow limiter (i, o, or io) //------------------------------------------------------- + mGuiIgnoresArguments = true; + if (mGuiEnabled) { + break; + } char cmd[]{"--overflowlimiting (-O)"}; if (gVerboseFlag) { printf("%s argument = %s\n", cmd, optarg); @@ -554,6 +590,10 @@ void Settings::parseInput(int argc, char** argv) } case 'a': { // assumed number of clients (applies to outgoing limiter) //------------------------------------------------------- + mGuiIgnoresArguments = true; + if (mGuiEnabled) { + break; + } char cmd[]{"--assumednumclients (-a)"}; if (gVerboseFlag) { printf("%s argument = %s\n", cmd, optarg); @@ -571,6 +611,10 @@ void Settings::parseInput(int argc, char** argv) } case 'f': { // --effects (-f) effectsSpecArg //------------------------------------------------------- + mGuiIgnoresArguments = true; + if (mGuiEnabled) { + break; + } char cmd[]{"--effects (-f)"}; int returnCode = mEffects.parseEffectsOptArg(cmd, optarg); if (returnCode > 1) { @@ -596,13 +640,27 @@ void Settings::parseInput(int argc, char** argv) mCredsFile = optarg; break; case OPT_AUTHUSER: + // Need to manually check if we have our optional argument. + // (getopt_long will only find an optional parameter for long arguments if + // there's a '=' rather than a space between them. If we don't manually check, + // the paramater will be interpreted as an unknown argument.) + if (optarg == NULL && optind < argc && argv[optind][0] != '-') { + optarg = argv[optind++]; + } mUsername = optarg; break; case OPT_AUTHPASS: + if (optarg == NULL && optind < argc && argv[optind][0] != '-') { + optarg = argv[optind++]; + } mPassword = optarg; break; case 'x': { // examine connection (test mode) //------------------------------------------------------- + mGuiIgnoresArguments = true; + if (mGuiEnabled) { + break; + } char cmd[]{"--examine-audio-delay (-x)"}; if (tolower(optarg[0]) == 'h') { mAudioTester->printHelp(cmd, ch); @@ -620,6 +678,15 @@ void Settings::parseInput(int argc, char** argv) mAudioTester->setPrintIntervalSec(atof(optarg)); break; } + // The following two options need to be handled earlier, so are all parsed in + // main. Included here so that we don't get an unrecognized option error. + case OPT_GUI: + break; + case OPT_DEEPLINK: + if (optarg == NULL && optind < argc && argv[optind][0] != '-') { + optarg = argv[optind++]; + } + break; case ':': { printUsage(); printf("*** Missing option argument *** see above for usage\n\n"); @@ -641,6 +708,7 @@ void Settings::parseInput(int argc, char** argv) break; } } + } // Warn user if undefined options where entered //---------------------------------------------------------------------------- @@ -669,6 +737,11 @@ void Settings::parseInput(int argc, char** argv) } // Exit if options are incompatible //---------------------------------------------------------------------------- + if (mGuiEnabled) { + // The following tests aren't yet needed for the supported GUI options. + return; + } + bool haveSomeServerMode = not((mJackTripMode == JackTrip::CLIENT) || (mJackTripMode == JackTrip::CLIENTTOPINGSERVER)); if (mEffects.getHaveEffect() && haveSomeServerMode) { @@ -824,17 +897,17 @@ void Settings::printUsage() "(sources) mixing at Hub Server (otherwise 2 assumed by -O)" << endl; cout << endl; + cout << " -T, --srate # Set the sampling rate, works only " + "on some audio backends (default: 48000)" + << endl; + cout << " -F, --bufsize # Set the buffer size, works only " + "on some audio backends (default: 128)" + << endl; #ifdef RT_AUDIO cout << "ARGUMENTS TO USE JACKTRIP WITHOUT JACK:" << endl; cout << " -R, --rtaudio Use system's default sound system " "instead of Jack" << endl; - cout << " -T, --srate # Set the sampling rate, works on " - "--rtaudio mode only (default: 48000)" - << endl; - cout << " -F, --bufsize # Set the buffer size, works on " - "--rtaudio mode only (default: 128)" - << endl; cout << " --audiodevice \"input-output device name\"" << endl; cout << " --audiodevice \"input device name\",\"output device name\"" << endl; cout << " --audioinputdevice \"input device name\"" << endl; @@ -875,6 +948,9 @@ void Settings::printUsage() cout << " --username The username to use when connecting as a hub client (if not supplied here, this is read from standard input)" << endl; cout << " --password The password to use when connecting as a hub client (if not supplied here, this is read from standard input)" << endl; cout << endl; + cout << "ARGUMENTS FOR THE GUI:" << endl; + cout << " --gui Force JackTrip to run with the GUI. If not using VirtualStudio mode, command line switches in the required arguments, optional arguments (except -l, -j, -L, --appendthreadid), audio patching, and authentication sections will be honoured, and default settings will be used where arguments aren't supplied. Options from other sections will be ignored (and the last used settings will be loaded), except for -V, and the --version and --help switches which will override this." << endl; + cout << endl; cout << "HELP ARGUMENTS: " << endl; cout << " -v, --version Prints Version Number" << endl; cout << " -V, --verbose Verbose mode, prints debug messages" @@ -977,7 +1053,8 @@ JackTrip* Settings::getConfiguredJackTrip() if (gVerboseFlag) std::cout << "Settings:startJackTrip before new JackTrip" << std::endl; JackTrip* jackTrip = new JackTrip( - mJackTripMode, mDataProtocol, mNumAudioInputChans, mNumAudioOutputChans, + mJackTripMode, mDataProtocol, mBaseAudioInputChanNum, mNumAudioInputChans, + mBaseAudioOutputChanNum, mNumAudioOutputChans, AudioInterface::MIX_UNSET, #ifdef WAIR // wair mNumNetRevChans, #endif // endwhere @@ -1028,15 +1105,36 @@ JackTrip* Settings::getConfiguredJackTrip() jackTrip->setPacketHeaderType(DataProtocol::EMPTY); } - // Set RtAudio -#ifdef RT_AUDIO - if (!mUseJack) { - jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); + // Change default Buffer Size + if (mChangeDefaultBS) { + jackTrip->setAudioBufferSizeInSamples(mAudioBufferSize); + if (!mChangeDefaultSR) { + jackTrip->setSampleRate(48000); + mSampleRate = 48000; + } } // Change default Sampling Rate if (mChangeDefaultSR) { jackTrip->setSampleRate(mSampleRate); + if (!mChangeDefaultBS) { + jackTrip->setAudioBufferSizeInSamples(128); + mAudioBufferSize = 128; + } + } + +#if defined(__unix__) + if (mChangeDefaultBS or mChangeDefaultSR) { + char latency_env[40]; + sprintf(latency_env, "%d/%d", mAudioBufferSize, mSampleRate); + setenv("PIPEWIRE_LATENCY", latency_env, 1); + } +#endif + + // Set RtAudio +#ifdef RT_AUDIO + if (!mUseJack) { + jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); } // Change default device ID @@ -1044,11 +1142,6 @@ JackTrip* Settings::getConfiguredJackTrip() jackTrip->setDeviceID(mDeviceID); } - // Change default Buffer Size - if (mChangeDefaultBS) { - jackTrip->setAudioBufferSizeInSamples(mAudioBufferSize); - } - // Set device names jackTrip->setInputDevice(mInputDeviceName); jackTrip->setOutputDevice(mOutputDeviceName); @@ -1176,3 +1269,15 @@ void Settings::disableEcho(bool disabled) tcsetattr(STDIN_FILENO, TCSANOW, &tty); #endif } + +void Settings::checkMode() +{ + if (mModeSet) { + std::cerr + << "Conflicting arguments given. Please choose only one of -c, -s, -C or -S." + << std::endl; + std::exit(1); + } else { + mModeSet = true; + } +} diff --git a/src/Settings.h b/src/Settings.h index ba10b8a..6b556f8 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -60,7 +60,12 @@ class Settings : public QObject Q_OBJECT; public: - Settings(QObject* parent = nullptr) : QObject(parent), mAudioTester(new AudioTester) + Settings(bool guiEnabled = false, QObject* parent = nullptr) + : QObject(parent) +#ifndef NO_GUI + , mGuiEnabled(guiEnabled) +#endif + , mAudioTester(new AudioTester) { } @@ -77,16 +82,56 @@ class Settings : public QObject #endif bool getLoopBack() { return mLoopBack; } - bool isHubServer() { return mJackTripServer; } + bool isHubServer() { return mJackTripMode == JackTrip::SERVERPINGSERVER; } + bool guiIgnoresArguments() { return mGuiIgnoresArguments; } + bool isModeSet() { return mModeSet; } + + JackTrip::jacktripModeT getJackTripMode() { return mJackTripMode; } + int getNumAudioInputChans() { return mNumAudioInputChans; } + int getNumAudioOutputChans() { return mNumAudioOutputChans; } + int getQueueLength() { return mBufferQueueLength; } + unsigned int getRedundancy() { return mRedundancy; } + QString getPeerAddress() { return mPeerAddress; } + int getBindPort() { return mBindPortNum; } + int getPeerPort() { return mPeerPortNum; } + int getServerUdpPort() { return mServerUdpPortNum; } + AudioInterface::audioBitResolutionT getAudioBitResolution() + { + return mAudioBitResolution; + } + JackTrip::underrunModeT getUnderrunMode() { return mUnderrunMode; } + bool getStopOnTimeout() { return mStopOnTimeout; } + QString getClientName() { return mClientName; } + QString getRemoteClientName() { return mRemoteClientName; } + bool getConnectDefaultAudioPorts() { return mConnectDefaultAudioPorts; } + int getBufferStrategy() { return mBufferStrategy; } + int getBroadCastQueue() { return mBroadcastQueue; } + bool getUseRtUdpPriority() { return mUseRtUdpPriority; } + unsigned int getHubConnectionMode() { return mHubConnectionMode; } + bool getPatchServerAudio() { return mPatchServerAudio; } + bool getStereoUpmix() { return mStereoUpmix; } + bool getUseAuthentication() { return mAuth; } + QString getCertFile() { return mCertFile; } + QString getKeyFile() { return mKeyFile; } + QString getCredsFile() { return mCredsFile; } + QString getUsername() { return mUsername; } + QString getPassword() { return mPassword; } private: void disableEcho(bool disabled); + void checkMode(); + + bool mGuiEnabled = false; + bool mGuiIgnoresArguments = false; JackTrip::jacktripModeT mJackTripMode = - JackTrip::SERVER; ///< JackTrip::jacktripModeT + JackTrip::SERVER; ///< JackTrip::jacktripModeT + bool mModeSet = false; JackTrip::dataProtocolT mDataProtocol = JackTrip::UDP; ///< Data Protocol int mNumAudioInputChans = 2; ///< Number of Input Channels int mNumAudioOutputChans = 2; ///< Number of Output Channels + int mBaseAudioInputChanNum = 0; ///< Base Input Channel Number + int mBaseAudioOutputChanNum = 0; ///< Base Output Channel Number int mBufferQueueLength = gDefaultQueueLength; ///< Audio Buffer from network queue length AudioInterface::audioBitResolutionT mAudioBitResolution = AudioInterface::BIT16; @@ -111,17 +156,18 @@ class Settings : public QObject bool mLoopBack = false; ///< Loop-back mode bool mJamLink = false; ///< JamLink mode bool mEmptyHeader = false; ///< EmptyHeader mode - bool mJackTripServer = false; ///< JackTrip Server mode QString mLocalAddress = gDefaultLocalAddress; ///< Local Address unsigned int mRedundancy = 1; ///< Redundancy factor for data in the network bool mUseJack = true; ///< Use or not JackAduio bool mChangeDefaultSR = false; ///< Change Default Sampling Rate bool mChangeDefaultID = 0; ///< Change Default device ID bool mChangeDefaultBS = false; ///< Change Default Buffer Size -#ifdef RT_AUDIO + unsigned int mSampleRate; - unsigned int mDeviceID; unsigned int mAudioBufferSize; + +#ifdef RT_AUDIO + unsigned int mDeviceID; std::string mInputDeviceName, mOutputDeviceName; #endif unsigned int mHubConnectionMode = JackTrip::SERVERTOCLIENT; diff --git a/src/StereoToMono.cpp b/src/StereoToMono.cpp new file mode 100644 index 0000000..bd78e21 --- /dev/null +++ b/src/StereoToMono.cpp @@ -0,0 +1,73 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file StereoToMono.cpp + * \author Dominick Hing + * \date February 2023 + */ + +#include "StereoToMono.h" + +#include + +#include "jacktrip_types.h" + +//******************************************************************************* +void StereoToMono::init(int samplingRate) +{ + ProcessPlugin::init(samplingRate); + if (samplingRate != fSamplingFreq) { + std::cerr << "Sampling rate not set by superclass!\n"; + std::exit(1); + } + + fs = float(fSamplingFreq); + stereoToMonoP->init(fs); + + inited = true; +} + +//******************************************************************************* +void StereoToMono::compute(int nframes, float** inputs, float** outputs) +{ + if (not inited) { + std::cerr << "*** Stereo-to-Mono " << this + << ": init never called! Doing it now.\n"; + if (fSamplingFreq <= 0) { + fSamplingFreq = 48000; + std::cout << "Stereo-to-Mono " << this + << ": *** HAD TO GUESS the sampling rate (chose 48000 Hz) ***\n"; + } + init(fSamplingFreq); + } + stereoToMonoP->compute(nframes, inputs, outputs); +} \ No newline at end of file diff --git a/src/StereoToMono.h b/src/StereoToMono.h new file mode 100644 index 0000000..3fdbcaf --- /dev/null +++ b/src/StereoToMono.h @@ -0,0 +1,80 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file StereoToMono.h + * \author Dominick Hing + * \date February 2023 + * \license MIT + */ + +#ifndef __STEREOTOMONO_H__ +#define __STEREOTOMONO_H__ + +#include +#include +#include +#include + +#include "ProcessPlugin.h" +#include "stereotomonodsp.h" + +/** \brief The Meter class measures the live audio loudness level + */ +class StereoToMono : public ProcessPlugin +{ + Q_OBJECT; + + public: + /// \brief The class constructor sets the number of channels to measure + StereoToMono(bool verboseFlag = false) + { + setVerbose(verboseFlag); + stereoToMonoP = new stereotomonodsp; + } + + /// \brief The class destructor + virtual ~StereoToMono() { delete stereoToMonoP; } + + void init(int samplingRate) override; + int getNumInputs() override { return 2; } + int getNumOutputs() override { return 2; } + void compute(int nframes, float** inputs, float** outputs) override; + const char* getName() const override { return "Stereo-to-Mono"; }; + + private: + float fs; + // int mNumChannels; + stereotomonodsp* stereoToMonoP; + // bool hasProcessedAudio = false; +}; + +#endif \ No newline at end of file diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index bfd862c..df281d7 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -635,7 +635,7 @@ void UdpDataProtocol::run() dataBuf.len = full_redundant_packet_size; dataBuf.buf = reinterpret_cast(full_redundant_packet); DWORD recvBytes = 0, flags = 0, index, bytesTransferred = 0; - + eventArray[0] = WSACreateEvent(); if (eventArray == WSA_INVALID_EVENT) { emit signalError("Unable to set up network event monitoring"); @@ -644,7 +644,7 @@ void UdpDataProtocol::run() } ZeroMemory(&socketOverlapped, sizeof(WSAOVERLAPPED)); socketOverlapped.hEvent = eventArray[0]; - + if (WSARecv(mSocket, &dataBuf, 1, &recvBytes, &flags, &socketOverlapped, NULL) == SOCKET_ERROR) { int result = WSAGetLastError(); if (result != WSA_IO_PENDING) { @@ -682,7 +682,7 @@ void UdpDataProtocol::run() } } #else - + // OLD CODE WITHOUT REDUNDANCY---------------------------------------------------- /* // This is blocking until we get a packet... diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp index b9bd65b..6508e6f 100644 --- a/src/UdpHubListener.cpp +++ b/src/UdpHubListener.cpp @@ -218,7 +218,7 @@ void UdpHubListener::start() emit signalError(error_message); return; } - mAuth.reset(new Auth(mCredsFile)); + mAuth.reset(new Auth(mCredsFile, true)); } cout << "JackTrip HUB SERVER: Waiting for client connections..." << endl; diff --git a/src/gui/AudioSettings.qml b/src/gui/AudioSettings.qml new file mode 100644 index 0000000..aa902f1 --- /dev/null +++ b/src/gui/AudioSettings.qml @@ -0,0 +1,1110 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +Rectangle { + width: parent.width + height: parent.height + color: backgroundColour + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property int leftMargin: 48 + property int rightMargin: 16 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + property int buttonWidth: 103 + property int buttonHeight: 25 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" + property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" + property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" + property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" + property string warningTextColour: "#DB0A0A" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property string toolTipTextColour: textColour + + property string errorFlagColour: "#DB0A0A" + property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + property bool isUsingJack: virtualstudio.audioBackend == "JACK" + property bool isUsingRtAudio: virtualstudio.audioBackend == "RtAudio" + property bool hasNoBackend: !isUsingJack && !isUsingRtAudio && !virtualstudio.backendAvailable; + + property int inputCurrIndex: getCurrentInputDeviceIndex() + property int outputCurrIndex: getCurrentOutputDeviceIndex() + + function getCurrentInputDeviceIndex () { + if (virtualstudio.inputDevice === "") { + return inputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = inputComboModel.findIndex(elem => elem.type === "element" && elem.text === virtualstudio.inputDevice); + if (idx < 0) { + idx = inputComboModel.findIndex(elem => elem.type === "element"); + } + return idx; + } + + function getCurrentOutputDeviceIndex() { + if (virtualstudio.outputDevice === "") { + return outputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = outputComboModel.findIndex(elem => elem.type === "element" && elem.text === virtualstudio.outputDevice); + if (idx < 0) { + idx = outputComboModel.findIndex(elem => elem.type === "element"); + } + return idx; + } + + Item { + id: usingRtAudio + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 24 * virtualstudio.uiScale + anchors.right: parent.right + + visible: parent.isUsingRtAudio + + Rectangle { + id: leftSpacer + x: 0; y: 0 + width: 144 * virtualstudio.uiScale + height: 0 + color: "transparent" + } + + Text { + id: outputLabel + x: 0; y: 0 + text: "Output Device" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Image { + id: outputHelpIcon + anchors.left: outputLabel.right + anchors.bottom: outputLabel.top + anchors.bottomMargin: -8 * virtualstudio.uiScale + source: "help.svg" + sourceSize: Qt.size(12 * virtualstudio.uiScale, 12 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + property bool showToolTip: false + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 0.8 : 0.2 + } + + MouseArea { + id: outputMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: outputHelpIcon.showToolTip = true + onExited: outputHelpIcon.showToolTip = false + } + + ToolTip { + visible: outputHelpIcon.showToolTip + contentItem: Rectangle { + color: toolTipBackgroundColour + radius: 3 + anchors.fill: parent + anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale + anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale + layer.enabled: true + border.width: 1 + border.color: buttonStroke + + Text { + anchors.centerIn: parent + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale} + text: qsTr("How you'll hear the studio audio") + color: toolTipTextColour + } + } + background: Rectangle { + color: "transparent" + } + } + } + + Image { + id: headphonesIcon + anchors.left: outputLabel.left + anchors.verticalCenter: outputDeviceMeters.verticalCenter + source: "headphones.svg" + sourceSize: Qt.size(28 * virtualstudio.uiScale, 28 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + ComboBox { + id: outputCombo + anchors.left: leftSpacer.right + anchors.verticalCenter: outputLabel.verticalCenter + anchors.rightMargin: rightMargin * virtualstudio.uiScale + width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale + model: outputComboModel + currentIndex: outputCurrIndex + delegate: ItemDelegate { + required property var modelData + required property int index + + leftPadding: 0 + + width: parent.width + contentItem: Text { + leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 + text: modelData.text + font.bold: modelData.type === "header" + } + highlighted: outputCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.type == "element") { + outputCombo.currentIndex = index + outputCombo.popup.close() + virtualstudio.outputDevice = modelData.text + virtualstudio.validateDevicesState() + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: outputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : "" + } + } + + Meter { + id: outputDeviceMeters + anchors.left: outputCombo.left + anchors.right: outputCombo.right + anchors.top: outputCombo.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + model: outputMeterModel + clipped: outputClipped + enabled: virtualstudio.audioReady && !Boolean(virtualstudio.devicesError) + } + + Slider { + id: outputSlider + from: 0.0 + value: audioInterface ? audioInterface.outputVolume : 0.5 + onMoved: { audioInterface.outputVolume = value } + to: 1.0 + padding: 0 + anchors.left: outputQuieterIcon.right + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.right: outputLouderIcon.left + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: outputDeviceMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + + background: Rectangle { + x: outputSlider.leftPadding + y: outputSlider.topPadding + outputSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: outputSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: outputSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: outputSlider.leftPadding + outputSlider.visualPosition * (outputSlider.availableWidth - width) + y: outputSlider.topPadding + outputSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: outputSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Image { + id: outputQuieterIcon + anchors.left: outputCombo.left + anchors.verticalCenter: outputSlider.verticalCenter + source: "quiet.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Image { + id: outputLouderIcon + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: outputSlider.verticalCenter + source: "loud.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Text { + id: outputChannelsLabel + anchors.left: outputCombo.left + anchors.right: outputCombo.horizontalCenter + anchors.top: outputSlider.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Output Channel(s)" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: outputChannelsCombo + anchors.left: outputCombo.left + anchors.right: outputCombo.horizontalCenter + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: outputChannelsLabel.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + model: outputChannelsComboModel + currentIndex: (() => { + let idx = outputChannelsComboModel.findIndex(elem => elem.baseChannel === virtualstudio.baseOutputChannel + && elem.numChannels === virtualstudio.numOutputChannels); + if (idx < 0) { + idx = 0; + } + return idx; + })() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: outputChannelsCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + outputChannelsCombo.currentIndex = index + outputChannelsCombo.popup.close() + virtualstudio.baseOutputChannel = modelData.baseChannel + virtualstudio.numOutputChannels = modelData.numChannels + virtualstudio.validateDevicesState() + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || "" + } + } + + Button { + id: testOutputAudioButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.playOutputAudio() } + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: outputChannelsCombo.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Play Test Tone" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Rectangle { + id: divider1 + anchors.top: testOutputAudioButton.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale + color: "#E0E0E0" + } + + Text { + id: inputLabel + anchors.left: outputLabel.left + anchors.top: divider1.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + text: "Input Device" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Image { + id: inputHelpIcon + anchors.left: inputLabel.right + anchors.bottom: inputLabel.top + anchors.bottomMargin: -8 * virtualstudio.uiScale + source: "help.svg" + sourceSize: Qt.size(12 * virtualstudio.uiScale, 12 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + property bool showToolTip: false + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 0.8 : 0.2 + } + + MouseArea { + id: inputMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: inputHelpIcon.showToolTip = true + onExited: inputHelpIcon.showToolTip = false + } + + ToolTip { + visible: inputHelpIcon.showToolTip + contentItem: Rectangle { + color: toolTipBackgroundColour + radius: 3 + anchors.fill: parent + anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale + anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale + layer.enabled: true + border.width: 1 + border.color: buttonStroke + + Text { + anchors.centerIn: parent + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale} + text: qsTr("Send audio to the studio (microphone, instrument, mixer, etc.)") + color: toolTipTextColour + } + } + background: Rectangle { + color: "transparent" + } + } + } + + Image { + id: microphoneIcon + anchors.left: outputLabel.left + anchors.verticalCenter: inputDeviceMeters.verticalCenter + source: "mic.svg" + sourceSize: Qt.size(32 * virtualstudio.uiScale, 32 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + ComboBox { + id: inputCombo + model: inputComboModel + currentIndex: inputCurrIndex + anchors.left: outputCombo.left + anchors.right: outputCombo.right + anchors.verticalCenter: inputLabel.verticalCenter + delegate: ItemDelegate { + required property var modelData + required property int index + + leftPadding: 0 + + width: parent.width + contentItem: Text { + leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 + text: modelData.text + font.bold: modelData.type === "header" + } + highlighted: inputCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.type == "element") { + inputCombo.currentIndex = index + inputCombo.popup.close() + virtualstudio.inputDevice = modelData.text + virtualstudio.validateDevicesState() + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : "" + } + } + + Meter { + id: inputDeviceMeters + anchors.left: inputCombo.left + anchors.right: inputCombo.right + anchors.top: inputCombo.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + model: inputMeterModel + clipped: inputClipped + enabled: virtualstudio.audioReady && !Boolean(virtualstudio.devicesError) + } + + Slider { + id: inputSlider + from: 0.0 + value: audioInterface ? audioInterface.inputVolume : 0.5 + onMoved: { audioInterface.inputVolume = value } + to: 1.0 + padding: 0 + anchors.left: inputQuieterIcon.right + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.right: inputLouderIcon.left + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputDeviceMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + + background: Rectangle { + x: inputSlider.leftPadding + y: inputSlider.topPadding + inputSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: inputSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: inputSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: inputSlider.leftPadding + inputSlider.visualPosition * (inputSlider.availableWidth - width) + y: inputSlider.topPadding + inputSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: inputSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Image { + id: inputQuieterIcon + anchors.left: inputDeviceMeters.left + anchors.verticalCenter: inputSlider.verticalCenter + source: "quiet.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Image { + id: inputLouderIcon + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: inputSlider.verticalCenter + source: "loud.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Button { + id: hiddenInputButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: inputSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: false + } + + Text { + id: inputChannelsLabel + anchors.left: inputCombo.left + anchors.right: inputCombo.horizontalCenter + anchors.top: inputSlider.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Input Channel(s)" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: inputChannelsCombo + anchors.left: inputCombo.left + anchors.right: inputCombo.horizontalCenter + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputChannelsLabel.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + model: inputChannelsComboModel + currentIndex: (() => { + let idx = inputChannelsComboModel.findIndex(elem => elem.baseChannel === virtualstudio.baseInputChannel + && elem.numChannels === virtualstudio.numInputChannels); + if (idx < 0) { + idx = 0; + } + return idx; + })() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: inputChannelsCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + inputChannelsCombo.currentIndex = index + inputChannelsCombo.popup.close() + virtualstudio.baseInputChannel = modelData.baseChannel + virtualstudio.numInputChannels = modelData.numChannels + virtualstudio.validateDevicesState() + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || "" + } + } + + Text { + id: inputMixModeLabel + anchors.left: inputCombo.horizontalCenter + anchors.right: inputCombo.right + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputSlider.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Mono / Stereo" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: inputMixModeCombo + anchors.left: inputCombo.horizontalCenter + anchors.right: inputCombo.right + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputMixModeLabel.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + model: inputMixModeComboModel + currentIndex: (() => { + let idx = inputMixModeComboModel.findIndex(elem => elem.value === virtualstudio.inputMixMode); + if (idx < 0) { + idx = 0; + } + return idx; + })() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: inputMixModeCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + inputMixModeCombo.currentIndex = index + inputMixModeCombo.popup.close() + virtualstudio.inputMixMode = inputMixModeComboModel[index].value + virtualstudio.validateDevicesState() + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || "" + } + } + + Text { + id: inputChannelHelpMessage + anchors.left: inputChannelsCombo.left + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.right: inputChannelsCombo.right + anchors.top: inputChannelsCombo.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + textFormat: Text.RichText + wrapMode: Text.WordWrap + text: "Choose up to 2 channels" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: inputMixModeHelpMessage + anchors.left: inputMixModeCombo.left + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.right: inputMixModeCombo.right + anchors.top: inputMixModeCombo.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + textFormat: Text.RichText + wrapMode: Text.WordWrap + text: (() => { + if (virtualstudio.inputMixMode === 2) { + return "Treat the channels as Left and Right signals, coming through each speaker separately."; + } else if (virtualstudio.inputMixMode === 3) { + return "Combine the channels into one central channel coming through both speakers."; + } else if (virtualstudio.inputMixMode === 1) { + return "Send a single channel of audio"; + } else { + return ""; + } + })() + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: warningOrErrorMessage + anchors.left: inputLabel.left + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.top: inputMixModeHelpMessage.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + anchors.bottomMargin: 8 * virtualstudio.uiScale + textFormat: Text.RichText + text: (virtualstudio.devicesError || virtualstudio.devicesWarning) + + ((virtualstudio.devicesErrorHelpUrl || virtualstudio.devicesWarningHelpUrl) + ? ` Learn More.` + : "" + ) + onLinkActivated: link => { + virtualstudio.openLink(link) + } + horizontalAlignment: Text.AlignHLeft + wrapMode: Text.WordWrap + color: warningTextColour + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + visible: Boolean(virtualstudio.devicesError) || Boolean(virtualstudio.devicesWarning); + } + } + + Item { + id: usingJACK + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: leftMargin * virtualstudio.uiScale + anchors.right: parent.right + + visible: parent.isUsingJack + + Text { + id: jackLabel + x: 0; y: 0 + width: parent.width - rightMargin * virtualstudio.uiScale + text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + + Text { + id: jackOutputLabel + anchors.left: jackLabel.left + anchors.top: jackLabel.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + width: 144 * virtualstudio.uiScale + text: "Output Volume" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + + Image { + id: jackHeadphonesIcon + anchors.left: jackOutputLabel.left + anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter + source: "headphones.svg" + sourceSize: Qt.size(28 * virtualstudio.uiScale, 28 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Meter { + id: jackOutputMeters + anchors.left: jackOutputLabel.right + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackOutputLabel.verticalCenter + height: 24 * virtualstudio.uiScale + model: outputMeterModel + clipped: outputClipped + enabled: virtualstudio.audioReady && !Boolean(virtualstudio.devicesError) + } + + Button { + id: jackTestOutputAudioButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.playOutputAudio() } + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Play Test Tone" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Slider { + id: jackOutputVolumeSlider + from: 0.0 + value: audioInterface ? audioInterface.outputVolume : 0.5 + onMoved: { audioInterface.outputVolume = value } + to: 1.0 + padding: 0 + anchors.left: jackOutputQuieterButton.right + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.right: jackOutputLouderIcon.left + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: jackOutputMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + + background: Rectangle { + x: jackOutputVolumeSlider.leftPadding + y: jackOutputVolumeSlider.topPadding + jackOutputVolumeSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: jackOutputVolumeSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: jackOutputVolumeSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: jackOutputVolumeSlider.leftPadding + jackOutputVolumeSlider.visualPosition * (jackOutputVolumeSlider.availableWidth - width) + y: jackOutputVolumeSlider.topPadding + jackOutputVolumeSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: jackOutputVolumeSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Image { + id: jackOutputQuieterButton + anchors.left: jackOutputMeters.left + anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter + source: "quiet.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Image { + id: jackOutputLouderIcon + anchors.right: jackTestOutputAudioButton.left + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter + source: "loud.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Text { + id: jackInputLabel + anchors.left: jackLabel.left + anchors.top: jackOutputVolumeSlider.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + width: 144 * virtualstudio.uiScale + text: "Input Volume" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + + Image { + id: jackMicrophoneIcon + anchors.left: jackInputLabel.left + anchors.verticalCenter: jackInputVolumeSlider.verticalCenter + source: "mic.svg" + sourceSize: Qt.size(32 * virtualstudio.uiScale, 32 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Meter { + id: jackInputMeters + anchors.left: jackInputLabel.right + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackInputLabel.verticalCenter + height: 24 * virtualstudio.uiScale + model: inputMeterModel + clipped: inputClipped + enabled: virtualstudio.audioReady && !Boolean(virtualstudio.devicesError) + } + + Slider { + id: jackInputVolumeSlider + from: 0.0 + value: audioInterface ? audioInterface.inputVolume : 0.5 + onMoved: { audioInterface.inputVolume = value } + to: 1.0 + padding: 0 + anchors.left: jackInputQuieterButton.right + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.right: jackInputLouderIcon.left + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: jackInputMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + + background: Rectangle { + x: jackInputVolumeSlider.leftPadding + y: jackInputVolumeSlider.topPadding + jackInputVolumeSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: jackInputVolumeSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: jackInputVolumeSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: jackInputVolumeSlider.leftPadding + jackInputVolumeSlider.visualPosition * (jackInputVolumeSlider.availableWidth - width) + y: jackInputVolumeSlider.topPadding + jackInputVolumeSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: jackInputVolumeSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Image { + id: jackInputQuieterButton + anchors.left: jackInputMeters.left + anchors.verticalCenter: jackInputVolumeSlider.verticalCenter + source: "quiet.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Image { + id: jackInputLouderIcon + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackInputVolumeSlider.verticalCenter + source: "loud.svg" + sourceSize: Qt.size(16 * virtualstudio.uiScale, 16 * virtualstudio.uiScale) + fillMode: Image.PreserveAspectFit + smooth: true + + Colorize { + anchors.fill: parent + source: parent + hue: 0 + saturation: 0 + lightness: virtualstudio.darkMode ? 1 : 0 + } + } + + Button { + id: jackHiddenInputButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackInputVolumeSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: false + } + + } + + Item { + id: noBackend + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: leftMargin * virtualstudio.uiScale + anchors.right: parent.right + + visible: parent.hasNoBackend + + Text { + id: noBackendLabel + x: 0; y: 0 + width: parent.width - (16 * virtualstudio.uiScale) + text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + } +} \ No newline at end of file diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml index 01026db..4b6e719 100644 --- a/src/gui/Browse.qml +++ b/src/gui/Browse.qml @@ -1,6 +1,5 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 Item { width: parent.width; height: parent.height @@ -32,6 +31,7 @@ Item { property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D" function refresh() { scrollY = studioListView.contentY; @@ -81,7 +81,7 @@ Item { flagImage: bannerURL ? bannerURL : flag studioName: name publicStudio: isPublic - manageable: isManageable + admin: isAdmin available: canConnect connected: false studioId: id ? id : "" @@ -181,15 +181,8 @@ Item { radius: 6 * virtualstudio.uiScale color: createButton.down ? "#E7E8E8" : "#F2F3F3" border.width: 1 - border.color: createButton.down ? "#B0B5B5" : "#EAEBEB" + border.color: createButton.down ? "#B0B5B5" : createButtonStroke layer.enabled: createButton.hovered && !createButton.down - layer.effect: DropShadow { - horizontalOffset: 1 * virtualstudio.uiScale - verticalOffset: 1 * virtualstudio.uiScale - radius: 8.0 * virtualstudio.uiScale - samples: 17 - color: "#80A1A1A1" - } } onClicked: { virtualstudio.createStudio(); } anchors.top: createStudioMessage.bottom diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml index bd728c9..c152818 100644 --- a/src/gui/Connected.qml +++ b/src/gui/Connected.qml @@ -21,7 +21,7 @@ Item { property string studioStatus: (virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].status : "") property bool showReadyScreen: studioStatus === "Ready" property bool showStartingScreen: studioStatus === "Starting" - property bool showStoppingScreen: (virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable && !serverModel[virtualstudio.currentStudio].enabled && serverModel[virtualstudio.currentStudio].cloudId !== "" : false) + property bool showStoppingScreen: (virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isAdmin && !serverModel[virtualstudio.currentStudio].enabled && serverModel[virtualstudio.currentStudio].cloudId !== "" : false) property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" @@ -43,9 +43,13 @@ Item { property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" + property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" + property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" property string toolTipTextColour: textColour + property string warningTextColour: "#DB0A0A" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" property string meterGreen: "#61C554" property string meterYellow: "#F5BF4F" @@ -109,7 +113,7 @@ Item { flagImage: virtualstudio.currentStudio >= 0 ? ( serverModel[virtualstudio.currentStudio].bannerURL ? serverModel[virtualstudio.currentStudio].bannerURL : 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 + admin: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isAdmin : false available: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].canConnect : false studioId: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].id : "" inviteKeyString: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].inviteKey : "" @@ -127,7 +131,7 @@ Item { id: mic source: "mic.svg" x: 0; y: 0 - width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale sourceSize: Qt.size(mic.width,mic.height) fillMode: Image.PreserveAspectFit smooth: true @@ -158,7 +162,7 @@ Item { anchors.top: inputDeviceHeader.bottom anchors.left: inputDeviceHeader.left text: virtualstudio.audioBackend == "JACK" ? - virtualstudio.audioBackend : inputComboModel.filter(item => item.type === "element")[virtualstudio.inputDevice].text + virtualstudio.audioBackend : virtualstudio.inputDevice font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour elide: Text.ElideRight @@ -208,7 +212,7 @@ Item { anchors.top: outputDeviceHeader.bottom anchors.left: outputDeviceHeader.left text: virtualstudio.audioBackend == "JACK" ? - virtualstudio.audioBackend : outputComboModel.filter(item => item.type === "element")[virtualstudio.outputDevice].text + virtualstudio.audioBackend : virtualstudio.outputDevice font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour elide: Text.ElideRight @@ -243,6 +247,25 @@ Item { anchors.leftMargin: 8 * virtualstudio.uiScale anchors.right: inputDeviceMeters.right opacity: virtualstudio.inputMuted ? 0.3 : 1 + + background: Rectangle { + x: inputSlider.leftPadding + y: inputSlider.topPadding + inputSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: inputSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: inputSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + handle: Rectangle { x: inputSlider.leftPadding + inputSlider.visualPosition * (inputSlider.availableWidth - width) y: inputSlider.topPadding + inputSlider.availableHeight / 2 - height / 2 @@ -269,7 +292,7 @@ Item { onClicked: { virtualstudio.inputMuted = !virtualstudio.inputMuted } Image { id: micMute - width: 11.57 * virtualstudio.uiScale; height: 18 * virtualstudio.uiScale + width: 18 * virtualstudio.uiScale; height: 18 * virtualstudio.uiScale anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } source: virtualstudio.inputMuted ? "micoff.svg" : "mic.svg" sourceSize: Qt.size(micMute.width,micMute.height) @@ -296,13 +319,8 @@ Item { anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale layer.enabled: true - layer.effect: DropShadow { - horizontalOffset: 1 * virtualstudio.uiScale - verticalOffset: 1 * virtualstudio.uiScale - radius: 10.0 * virtualstudio.uiScale - samples: 21 - color: shadowColour - } + border.width: 1 + border.color: buttonStroke Text { anchors.centerIn: parent @@ -343,6 +361,25 @@ Item { y: outputDeviceMeters.y + 36 * virtualstudio.uiScale anchors.left: outputDeviceMeters.left anchors.right: outputDeviceMeters.right + + background: Rectangle { + x: outputSlider.leftPadding + y: outputSlider.topPadding + outputSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: outputSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: outputSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + handle: Rectangle { x: outputSlider.leftPadding + outputSlider.visualPosition * (outputSlider.availableWidth - width) y: outputSlider.topPadding + outputSlider.availableHeight / 2 - height / 2 @@ -395,7 +432,7 @@ Item { visible: showReadyScreen x: networkStatsHeader.x + networkStatsHeader.width; y: 410 * virtualstudio.uiScale width: parent.width - networkStatsHeader.width - 2 * bodyMargin * virtualstudio.uiScale - height: 128 * virtualstudio.uiScale + height: 72 * virtualstudio.uiScale Text { id: netstat0 @@ -416,13 +453,40 @@ Item { } } + Item { + id: devicesWarning + visible: showReadyScreen && Boolean(virtualstudio.devicesWarning) + x: bodyMargin * virtualstudio.uiScale + width: parent.width - (2 * x) + anchors.top: networkStatsText.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + + Text { + x: 0; y: 0 + width: devicesWarning.width + textFormat: Text.RichText + text: (virtualstudio.devicesWarning) + + ((virtualstudio.devicesWarningHelpUrl) + ? ` Learn More.` + : "" + ) + onLinkActivated: link => { + virtualstudio.openLink(link) + } + horizontalAlignment: Text.AlignHLeft + wrapMode: Text.WordWrap + font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: warningTextColour + } + } + Item { id: waitingScreen visible: showWaitingScreen x: bodyMargin * virtualstudio.uiScale; y: 230 * virtualstudio.uiScale width: parent.width - (2 * x) - property bool isManageable: (virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable : false) + property bool isAdmin: (virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isAdmin : false) Text { id: waitingText0 @@ -430,7 +494,7 @@ Item { width: parent.width color: textColour font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - text: parent.isManageable + text: parent.isAdmin ? "Waiting for this studio to start. Please start the studio using one of the options below." : "This studio is currently inactive. Please contact an owner or admin for this studio to start it." wrapMode: Text.WordWrap @@ -441,7 +505,7 @@ Item { anchors.top: waitingText0.bottom anchors.topMargin: 16 * virtualstudio.uiScale anchors.bottomMargin: 16 * virtualstudio.uiScale - visible: parent.isManageable + visible: parent.isAdmin height: 64 * virtualstudio.uiScale @@ -494,10 +558,10 @@ Item { x: 0 width: parent.width color: textColour - anchors.top: parent.isManageable ? startButtonsBox.bottom : waitingText0.bottom + anchors.top: parent.isAdmin ? startButtonsBox.bottom : waitingText0.bottom anchors.topMargin: 16 * virtualstudio.uiScale anchors.bottomMargin: 16 * virtualstudio.uiScale - visible: parent.isManageable + visible: parent.isAdmin font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } text: "You will be automatically connected to the studio when it is ready." wrapMode: Text.WordWrap diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml index a27d55a..f80ad8f 100644 --- a/src/gui/FirstLaunch.qml +++ b/src/gui/FirstLaunch.qml @@ -1,6 +1,5 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 Item { width: parent.width; height: parent.height @@ -8,11 +7,11 @@ Item { 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 buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" + property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF" - property string buttonHoverStroke: virtualstudio.darkMode ? "#777575" : "#DEDFDF" + property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" Image { @@ -52,13 +51,6 @@ Item { 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: { virtualstudio.showFirstRun = false; virtualstudio.windowState = "login"; virtualstudio.toVirtualStudio(); } x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale @@ -100,13 +92,6 @@ Item { 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: { virtualstudio.windowState = "login"; virtualstudio.toStandard(); } x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale diff --git a/src/gui/Login.qml b/src/gui/Login.qml index 955cfb1..c4b2b25 100644 --- a/src/gui/Login.qml +++ b/src/gui/Login.qml @@ -1,6 +1,5 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 Item { width: parent.width; height: parent.height @@ -18,7 +17,9 @@ Item { 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 buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" @@ -84,16 +85,9 @@ Item { background: Rectangle { radius: 6 * virtualstudio.uiScale color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour) - border.width: loginButton.down ? 1 : 0 - border.color: buttonStroke + border.width: 1 + border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : 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 @@ -116,16 +110,9 @@ Item { background: Rectangle { radius: 6 * virtualstudio.uiScale color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour) - border.width: backButton.down ? 1 : 0 - border.color: buttonStroke + border.width: 1 + border.color: backButton.down ? buttonPressedStroke : (backButton.hovered ? buttonHoverStroke : 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: { virtualstudio.windowState = "start" } anchors.horizontalCenter: parent.horizontalCenter diff --git a/src/gui/SectionHeading.qml b/src/gui/SectionHeading.qml index a996d0e..e4ab9ef 100644 --- a/src/gui/SectionHeading.qml +++ b/src/gui/SectionHeading.qml @@ -1,8 +1,9 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 Rectangle { + property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property bool listIsEmpty: false // required property string section: section (for 5.15) color: "transparent" @@ -30,13 +31,6 @@ Rectangle { 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 @@ -62,13 +56,6 @@ Rectangle { 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 @@ -95,15 +82,8 @@ Rectangle { radius: 6 * virtualstudio.uiScale color: "#F6F8F8" border.width: 1 - border.color: "#34979797" + border.color: filterStroke 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 diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml index e033a19..915fe2b 100644 --- a/src/gui/Settings.qml +++ b/src/gui/Settings.qml @@ -1,5 +1,6 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 Item { width: parent.width; height: parent.height @@ -10,7 +11,7 @@ Item { color: backgroundColour } - property int fontBig: 28 + property int fontBig: 20 property int fontMedium: 13 property int fontSmall: 11 property int fontExtraSmall: 8 @@ -28,18 +29,64 @@ Item { property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" + property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" + property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" + property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" property string warningTextColour: "#DB0A0A" property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" property string errorFlagColour: "#DB0A0A" - property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" property string settingsGroupView: "Audio" + property int inputCurrIndex: getCurrentInputDeviceIndex() + property int outputCurrIndex: getCurrentOutputDeviceIndex() + + function getCurrentInputDeviceIndex () { + if (virtualstudio.inputDevice === "") { + return inputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = inputComboModel.findIndex(elem => elem.type === "element" && elem.text === virtualstudio.inputDevice); + if (idx < 0) { + idx = inputComboModel.findIndex(elem => elem.type === "element"); + } + + return idx; + } + + function getCurrentOutputDeviceIndex() { + if (virtualstudio.outputDevice === "") { + return outputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = outputComboModel.findIndex(elem => elem.type === "element" && elem.text === virtualstudio.outputDevice); + if (idx < 0) { + idx = outputComboModel.findIndex(elem => elem.type === "element"); + } + + return idx; + } + + Rectangle { + id: audioSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + visible: settingsGroupView == "Audio" + + AudioSettings{ + id: audioSettings + } + } + ToolBar { id: header width: parent.width + height: 64 * virtualstudio.uiScale background: Rectangle { border.color: "#33979797" @@ -47,14 +94,62 @@ Item { width: parent.width } - contentItem: Label { - text: "Settings" - elide: Label.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour + contentItem: Item { + id: headerContent + width: header.width + height: header.height + x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale + + property bool isUsingRtAudio: virtualstudio.audioBackend == "RtAudio" + + Label { + id: pageTitle + text: "Settings" + height: headerContent.height; + anchors.left: headerContent.left; + anchors.leftMargin: 32 * virtualstudio.uiScale + elide: Label.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Button { + id: refreshButton + text: "Refresh Devices" + anchors.verticalCenter: pageTitle.verticalCenter; + anchors.right: headerContent.right; + anchors.rightMargin: 16 * virtualstudio.uiScale; + + palette.buttonText: textColour + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + icon { + source: "refresh.svg"; + color: textColour; + } + display: AbstractButton.TextBesideIcon + onClicked: { + virtualstudio.refreshDevices(); + inputCurrIndex = getCurrentInputDeviceIndex(); + outputCurrIndex = getCurrentOutputDeviceIndex(); + } + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + font { + family: "Poppins" + pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale + } + visible: parent.isUsingRtAudio && settingsGroupView == "Audio" + } + } + + } Drawer { @@ -104,8 +199,8 @@ Item { anchors.left: audioButtonText.right anchors.verticalCenter: audioButtonText.verticalCenter anchors.rightMargin: 16 * virtualstudio.uiScale - width: 8 * virtualstudio.uiScale - height: 8 * virtualstudio.uiScale + width: 8 * virtualstudio.uiScale + height: 8 * virtualstudio.uiScale color: errorFlagColour radius: 4 * virtualstudio.uiScale visible: Boolean(virtualstudio.devicesError) @@ -134,7 +229,7 @@ Item { color: textColour } } - + background: Rectangle { width: parent.width @@ -209,303 +304,6 @@ Item { } } - Rectangle { - id: audioSettingsView - width: 0.8 * parent.width - height: parent.height - header.height - x: 0.2 * window.width - y: header.height - color: backgroundColour - visible: settingsGroupView == "Audio" - - ComboBox { - id: backendCombo - model: backendComboModel - currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 - onActivated: { virtualstudio.audioBackend = currentText } - x: 234 * virtualstudio.uiScale; y: 48 * 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 && virtualstudio.backendAvailable - color: textColour - } - - Text { - id: noBackendLabel - x: leftMargin * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale - width: parent.width - x - (16 * virtualstudio.uiScale) - text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - visible: !virtualstudio.backendAvailable - 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 - } - - ComboBox { - id: outputCombo - model: outputComboModel - currentIndex: (() => { - let count = 0; - for (let i = 0; i < outputCombo.model.length; i++) { - if (outputCombo.model[i].type === "element") { - count++; - } - - if (count > virtualstudio.outputDevice) { - return i; - } - } - - return 0; - })() - x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 96 : 48) - width: backendCombo.width; height: backendCombo.height - visible: virtualstudio.audioBackend != "JACK" - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: outputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - outputCombo.currentIndex = index - outputCombo.popup.close() - virtualstudio.outputDevice = index - outputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: outputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : "" - } - } - - Button { - id: testOutputAudioButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: testOutputAudioButton.down ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.playOutputAudio() } - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - x: parent.width - (232 * virtualstudio.uiScale) - y: virtualstudio.audioBackend != "JACK" ? outputCombo.y + (48 * virtualstudio.uiScale) : jackLabel.y + (72 * virtualstudio.uiScale) - Text { - text: "Test Output Audio" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - visible: virtualstudio.audioReady - } - - Text { - id: inputLabel - anchors.verticalCenter: inputCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - anchors.top: virtualstudio.audioBackend != "JACK" ? inputCombo.top : inputDeviceMeters.top - anchors.topMargin: virtualstudio.audioBackend != "JACK" ? (inputCombo.height - inputLabel.height)/2 : 0 - text: "Input Device" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.backendAvailable - color: textColour - } - - ComboBox { - id: inputCombo - model: inputComboModel - currentIndex: (() => { - let count = 0; - for (let i = 0; i < inputCombo.model.length; i++) { - if (inputCombo.model[i].type === "element") { - count++; - } - - if (count > virtualstudio.inputDevice) { - return i; - } - } - - return 0; - })() - x: backendCombo.x; y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: inputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - inputCombo.currentIndex = index - inputCombo.popup.close() - virtualstudio.inputDevice = index - inputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : "" - } - } - - Meter { - id: inputDeviceMeters - anchors.left: backendCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? inputCombo.y + 48 * virtualstudio.uiScale : testOutputAudioButton.y + 72 * virtualstudio.uiScale - height: 100 * virtualstudio.uiScale - model: inputMeterModel - clipped: inputClipped - enabled: !Boolean(virtualstudio.devicesError) - visible: virtualstudio.audioReady - } - - Text { - anchors.left: backendCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? inputCombo.y + 48 * virtualstudio.uiScale : virtualstudio.uiScale * (virtualstudio.selectableBackend ? 112 : 64) - height: 100 * virtualstudio.uiScale - text: "Preparing audio..." - font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" && !virtualstudio.audioReady - 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: inputDeviceMeters.y + (48 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" - Text { - text: "Refresh Devices" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Text { - id: devicesWarningOrError - x: leftMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? refreshButton.y + (48 * virtualstudio.uiScale) : testOutputAudioButton.y + (48 * virtualstudio.uiScale) - width: parent.width - (64 * virtualstudio.uiScale) - textFormat: Text.RichText - text: (virtualstudio.devicesError || virtualstudio.devicesWarning) - + ((virtualstudio.devicesErrorHelpUrl || virtualstudio.devicesWarningHelpUrl) - ? ` Learn More.` - : "" - ) - onLinkActivated: link => { - virtualstudio.openLink(link) - } - horizontalAlignment: Text.AlignHLeft - wrapMode: Text.WordWrap - color: warningTextColour - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - visible: Boolean(virtualstudio.devicesError || virtualstudio.devicesWarning); - } - - Rectangle { - id: divider - x: leftMargin * virtualstudio.uiScale - y: Boolean(virtualstudio.devicesError || virtualstudio.devicesWarning) ? devicesWarningOrError.y + (60 * virtualstudio.uiScale) : refreshButton.y + (60 * virtualstudio.uiScale) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale - color: textColour - visible: virtualstudio.audioBackend != "JACK" - } - - ComboBox { - id: bufferCombo - x: backendCombo.x; y: divider.y + (24 * 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: appearanceSettingsView width: 0.8 * parent.width @@ -521,6 +319,34 @@ Item { width: backendCombo.width from: 1; to: 2; value: virtualstudio.uiScale onMoved: { virtualstudio.uiScale = value } + + background: Rectangle { + x: scaleSlider.leftPadding + y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: scaleSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: scaleSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width) + y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: scaleSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } } Text { @@ -615,9 +441,49 @@ Item { visible: !virtualstudio.noUpdater } + ComboBox { + id: backendCombo + model: backendComboModel + currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 + onActivated: { virtualstudio.audioBackend = currentText } + x: 234 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale) + width: updateChannelCombo.width; height: updateChannelCombo.height + 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 + } + + ComboBox { + id: bufferCombo + x: 234 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale) + width: backendCombo.width; height: updateChannelCombo.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 + } + ComboBox { id: bufferStrategyCombo - x: updateChannelCombo.x; y: updateChannelCombo.y + (48 * virtualstudio.uiScale) + x: updateChannelCombo.x; y: bufferCombo.y + (48 * virtualstudio.uiScale) width: updateChannelCombo.width; height: updateChannelCombo.height model: bufferStrategyComboModel currentIndex: virtualstudio.bufferStrategy @@ -750,8 +616,8 @@ Item { } onClicked: { virtualstudio.windowState = "browse"; - inputCombo.currentIndex = virtualstudio.previousInput; - outputCombo.currentIndex = virtualstudio.previousOutput; + inputCurrIndex = virtualstudio.previousInput; + outputCurrIndex = virtualstudio.previousOutput; virtualstudio.revertSettings() } anchors.verticalCenter: parent.verticalCenter diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml index 3fa839d..99c1ca5 100644 --- a/src/gui/Setup.qml +++ b/src/gui/Setup.qml @@ -6,7 +6,7 @@ Item { width: parent.width; height: parent.height clip: true - property int fontBig: 28 + property int fontBig: 20 property int fontMedium: 13 property int fontSmall: 11 property int fontExtraSmall: 8 @@ -28,6 +28,8 @@ Item { property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" + property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" + property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" property string saveButtonShadow: "#80A1A1A1" property string saveButtonBackgroundColour: "#F2F3F3" property string saveButtonPressedColour: "#E7E8E8" @@ -42,7 +44,7 @@ Item { property bool currShowWarnings: virtualstudio.showWarnings property string warningScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged") - + Item { id: ethernetWarningItem width: parent.width; height: parent.height @@ -107,15 +109,8 @@ Item { radius: 6 * virtualstudio.uiScale color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour border.width: 1 - border.color: okButtonEthernet.down ? saveButtonPressedStroke : saveButtonStroke + border.color: okButtonEthernet.down || okButtonEthernet.hovered ? 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 @@ -148,7 +143,7 @@ Item { x: showEthernetWarningCheckbox.leftPadding y: parent.height / 2 - height / 2 radius: 3 * virtualstudio.uiScale - border.color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + border.color: showEthernetWarningCheckbox.down || showEthernetWarningCheckbox.hovered ? checkboxPressedStroke : checkboxStroke Rectangle { width: 10 * virtualstudio.uiScale @@ -156,7 +151,7 @@ Item { x: 3 * virtualstudio.uiScale y: 3 * virtualstudio.uiScale radius: 2 * virtualstudio.uiScale - color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + color: showEthernetWarningCheckbox.down || showEthernetWarningCheckbox.hovered ? checkboxPressedStroke : checkboxStroke visible: showEthernetWarningCheckbox.checked } } @@ -256,15 +251,8 @@ Item { radius: 6 * virtualstudio.uiScale color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour border.width: 1 - border.color: okButtonHeadphones.down ? saveButtonPressedStroke : saveButtonStroke + border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? 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: { if (permissions.micPermission == "unknown") { @@ -303,7 +291,7 @@ Item { x: showHeadphonesWarningCheckbox.leftPadding y: parent.height / 2 - height / 2 radius: 3 * virtualstudio.uiScale - border.color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + border.color: showHeadphonesWarningCheckbox.down || showHeadphonesWarningCheckbox.hovered ? checkboxPressedStroke : checkboxStroke Rectangle { width: 10 * virtualstudio.uiScale @@ -311,7 +299,7 @@ Item { x: 3 * virtualstudio.uiScale y: 3 * virtualstudio.uiScale radius: 2 * virtualstudio.uiScale - color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke + color: showHeadphonesWarningCheckbox.down || showHeadphonesWarningCheckbox.hovered ? checkboxPressedStroke : checkboxStroke visible: showHeadphonesWarningCheckbox.checked } } @@ -372,17 +360,10 @@ Item { radius: 6 * virtualstudio.uiScale color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour border.width: 2 - border.color: showPromptButton.down ? saveButtonPressedStroke : saveButtonStroke + border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke layer.enabled: showPromptButton.hovered && !showPromptButton.down - layer.effect: DropShadow { - horizontalOffset: 1 * virtualstudio.uiScale - verticalOffset: 1 * virtualstudio.uiScale - radius: 8.0 * virtualstudio.uiScale - samples: 17 - color: saveButtonShadow - } } - onClicked: { + onClicked: { permissions.getMicPermission(); } anchors.right: microphonePrompt.right @@ -467,17 +448,10 @@ Item { radius: 6 * virtualstudio.uiScale color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour border.width: 1 - border.color: openSettingsButton.down ? saveButtonPressedStroke : saveButtonStroke + border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke layer.enabled: openSettingsButton.hovered && !openSettingsButton.down - layer.effect: DropShadow { - horizontalOffset: 1 * virtualstudio.uiScale - verticalOffset: 1 * virtualstudio.uiScale - radius: 8.0 * virtualstudio.uiScale - samples: 17 - color: saveButtonShadow - } } - onClicked: { + onClicked: { permissions.openSystemPrivacy(); } anchors.right: parent.right @@ -538,6 +512,8 @@ Item { width: parent.width; height: parent.height visible: (warningScreen == "acknowledged" || warningScreen == "microphone") && permissions.micPermission == "granted" + property bool isUsingRtAudio: virtualstudio.audioBackend == "RtAudio" + Text { id: pageTitle x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale @@ -546,305 +522,40 @@ Item { color: textColour } - ComboBox { - id: backendCombo - model: backendComboModel - currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 - onActivated: { virtualstudio.audioBackend = currentText } - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: pageTitle.y + 96 * virtualstudio.uiScale - width: parent.width - (234 * 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 && virtualstudio.backendAvailable - color: textColour - } - - Text { - id: noBackendLabel - x: leftMargin * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale - width: parent.width - x - (16 * virtualstudio.uiScale) - text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - visible: !virtualstudio.backendAvailable - color: textColour - } - - ComboBox { - id: outputCombo - model: outputComboModel - currentIndex: (() => { - let count = 0; - for (let i = 0; i < outputCombo.model.length; i++) { - if (outputCombo.model[i].type === "element") { - count++; - } - - if (count > virtualstudio.outputDevice) { - return i; - } - } - - return 0; - })() - x: backendCombo.x; y: backendCombo.y + virtualstudio.uiScale * (virtualstudio.selectableBackend ? 48 : 0) - width: backendCombo.width; height: backendCombo.height - visible: virtualstudio.audioBackend != "JACK" - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: outputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - outputCombo.currentIndex = index - outputCombo.popup.close() - virtualstudio.outputDevice = index - outputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: outputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : "" - } - } - - Text { - id: outputLabel - anchors.verticalCenter: virtualstudio.audioBackend != "JACK" ? outputCombo.verticalCenter : outputSlider.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: virtualstudio.audioBackend != "JACK" ? "Output Device" : "Output Volume" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: virtualstudio.backendAvailable - } - - Slider { - id: outputSlider - from: 0.0 - value: audioInterface ? audioInterface.outputVolume : 0.5 - onMoved: { audioInterface.outputVolume = value } - to: 1.0 - padding: 0 - y: virtualstudio.audioBackend != "JACK" ? outputCombo.y + 48 * virtualstudio.uiScale : jackLabel.y + 72 * virtualstudio.uiScale - anchors.left: outputCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - handle: Rectangle { - x: outputSlider.leftPadding + outputSlider.visualPosition * (outputSlider.availableWidth - width) - y: outputSlider.topPadding + outputSlider.availableHeight / 2 - height / 2 - implicitWidth: 26 * virtualstudio.uiScale - implicitHeight: 26 * virtualstudio.uiScale - radius: 13 * virtualstudio.uiScale - color: outputSlider.pressed ? sliderPressedColour : sliderColour - border.color: buttonStroke - } - visible: virtualstudio.backendAvailable - } - - Button { - id: testOutputAudioButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: testOutputAudioButton.down ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.playOutputAudio() } - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: outputSlider.y + (36 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: virtualstudio.audioReady - Text { - text: "Test Output Audio" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - ComboBox { - id: inputCombo - model: inputComboModel - currentIndex: (() => { - let count = 0; - for (let i = 0; i < inputCombo.model.length; i++) { - if (inputCombo.model[i].type === "element") { - count++; - } - - if (count > virtualstudio.inputDevice) { - return i; - } - } - - return 0; - })() - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: testOutputAudioButton.y + (48 * virtualstudio.uiScale) - width: parent.width - (234 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: inputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - inputCombo.currentIndex = index - inputCombo.popup.close() - virtualstudio.inputDevice = index - inputCombo.model.filter((elem, idx) => idx < index && elem.type === "header").length - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : "" - } - } - - Text { - id: inputLabel - anchors.top: virtualstudio.audioBackend != "JACK" ? inputCombo.top : inputDeviceMeters.top - anchors.topMargin: virtualstudio.audioBackend != "JACK" ? (inputCombo.height - inputLabel.height)/2 : 0 - x: leftMargin * virtualstudio.uiScale - text: virtualstudio.audioBackend != "JACK" ? "Input Device" : "Input Volume" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: virtualstudio.backendAvailable - } - - Meter { - id: inputDeviceMeters - anchors.left: backendCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - y: virtualstudio.audioBackend != "JACK" ? inputCombo.y + 72 * virtualstudio.uiScale : testOutputAudioButton.y + (72 * virtualstudio.uiScale) - height: 100 * virtualstudio.uiScale - model: inputMeterModel - clipped: inputClipped - enabled: !Boolean(virtualstudio.devicesError) - visible: virtualstudio.backendAvailable - } - - Slider { - id: inputSlider - from: 0.0 - value: audioInterface ? audioInterface.inputVolume : 0.5 - onMoved: { audioInterface.inputVolume = value } - to: 1.0 - padding: 0 - y: inputDeviceMeters.y + 48 * virtualstudio.uiScale - anchors.left: inputDeviceMeters.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - handle: Rectangle { - x: inputSlider.leftPadding + inputSlider.visualPosition * (inputSlider.availableWidth - width) - y: inputSlider.topPadding + inputSlider.availableHeight / 2 - height / 2 - implicitWidth: 26 * virtualstudio.uiScale - implicitHeight: 26 * virtualstudio.uiScale - radius: 13 * virtualstudio.uiScale - color: inputSlider.pressed ? sliderPressedColour : sliderColour - border.color: buttonStroke - } - visible: virtualstudio.backendAvailable - } - Button { id: refreshButton + text: "Refresh Devices" + palette.buttonText: textColour 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() } + icon { + source: "refresh.svg"; + color: textColour; + } + display: AbstractButton.TextBesideIcon + onClicked: { + virtualstudio.refreshDevices(); + } anchors.right: parent.right anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.topMargin: 18 * virtualstudio.uiScale - anchors.top: inputSlider.bottom - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" && virtualstudio.backendAvailable - Text { - text: "Refresh Devices" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour + anchors.verticalCenter: pageTitle.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + font { + family: "Poppins" + pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + visible: parent.isUsingRtAudio } - Text { - anchors.left: inputLabel.left - anchors.right: refreshButton.left - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.top: refreshButton.top - anchors.bottomMargin: 60 * virtualstudio.uiScale - textFormat: Text.RichText - text: (virtualstudio.devicesError || virtualstudio.devicesWarning) - + ((virtualstudio.devicesErrorHelpUrl || virtualstudio.devicesWarningHelpUrl) - ? ` Learn More.` - : "" - ) - onLinkActivated: link => { - virtualstudio.openLink(link) - } - horizontalAlignment: Text.AlignHLeft - wrapMode: Text.WordWrap - color: warningText - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - visible: Boolean(virtualstudio.devicesError) || Boolean(virtualstudio.devicesWarning); + AudioSettings { + id: audioSettings + width: parent.width + anchors.top: pageTitle.bottom + anchors.topMargin: 24 * virtualstudio.uiScale } Button { @@ -853,15 +564,7 @@ Item { 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 - } + border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke } enabled: !Boolean(virtualstudio.devicesError) && virtualstudio.backendAvailable onClicked: { virtualstudio.windowState = "browse"; virtualstudio.applySettings() } @@ -871,7 +574,7 @@ Item { anchors.bottom: parent.bottom width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale Text { - text: "Save Settings" + text: virtualstudio.studioToJoin.toString() ? "Connect to Studio" : "Save Settings" font.family: "Poppins" font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale font.weight: Font.Bold @@ -896,7 +599,7 @@ Item { x: showAgainCheckbox.leftPadding y: parent.height / 2 - height / 2 radius: 3 * virtualstudio.uiScale - border.color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke + border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke Rectangle { width: 10 * virtualstudio.uiScale @@ -904,7 +607,7 @@ Item { x: 3 * virtualstudio.uiScale y: 3 * virtualstudio.uiScale radius: 2 * virtualstudio.uiScale - color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke + color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke visible: showAgainCheckbox.checked } } diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml index f6808a3..2748519 100644 --- a/src/gui/Studio.qml +++ b/src/gui/Studio.qml @@ -7,17 +7,6 @@ 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" @@ -26,7 +15,7 @@ Rectangle { property string studioId: "" property string inviteKeyString: "" property bool publicStudio: false - property bool manageable: false + property bool admin: false property bool available: true property bool connected: false property bool inviteCopied: false @@ -45,6 +34,7 @@ Rectangle { property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" property string toolTipBackgroundColour: inviteCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3") property string toolTipTextColour: inviteCopied ? "#FAFBFB" : textColour + property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB" property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3" @@ -76,6 +66,11 @@ Rectangle { property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD" property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E" + property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + border.width: 1 + border.color: studioStroke + Clipboard { id: clipboard } @@ -87,15 +82,6 @@ Rectangle { 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 @@ -153,7 +139,7 @@ Rectangle { Text { x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale; - width: manageable ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale) + width: (admin || connected) ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale) text: studioName fontSizeMode: Text.HorizontalFit font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } @@ -181,7 +167,7 @@ Rectangle { Text { anchors.verticalCenter: publicRect.verticalCenter x: (leftMargin + 22) * virtualstudio.uiScale - width: manageable ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) + width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } elide: Text.ElideRight @@ -190,7 +176,7 @@ Rectangle { Button { id: joinButton - x: manageable ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) + x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width background: Rectangle { radius: width / 2 @@ -217,7 +203,7 @@ Rectangle { Button { id: leaveButton - x: manageable ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) + x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width background: Rectangle { radius: width / 2 @@ -251,7 +237,7 @@ Rectangle { Button { id: inviteButton - x: manageable ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale) + x: (admin || connected) ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale) y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width background: Rectangle { radius: width / 2 @@ -279,7 +265,7 @@ Rectangle { visible: true Image { id: shareImg - width: 20 * virtualstudio.uiScale; height: width + width: 24 * virtualstudio.uiScale; height: width anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } source: "share.svg" sourceSize: Qt.size(shareImg.width,shareImg.height) @@ -299,13 +285,8 @@ Rectangle { anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale layer.enabled: true - layer.effect: DropShadow { - horizontalOffset: 1 * virtualstudio.uiScale - verticalOffset: 1 * virtualstudio.uiScale - radius: 10.0 * virtualstudio.uiScale - samples: 21 - color: shadowColour - } + border.width: 1 + border.color: tooltipStroke Text { anchors.centerIn: parent @@ -330,30 +311,28 @@ Rectangle { } Button { - id: manageButton + id: manageOrVideoButton 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 + color: manageOrVideoButton.down ? managePressedColour : (manageOrVideoButton.hovered ? manageHoverColour : manageColour) + border.width: manageOrVideoButton.down ? 1 : 0 border.color: manageStroke } onClicked: { - if (manageable && connected) { + if (connected) { virtualstudio.launchVideo(-1) - } else if (connected) { - virtualstudio.manageStudio(-1); } else { virtualstudio.manageStudio(index); } } - visible: manageable + visible: admin || connected Image { id: manageImg width: 20 * virtualstudio.uiScale; height: width anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: manageable && connected ? "video.svg" : "manage.svg" + source: connected ? "video.svg" : "manage.svg" sourceSize: Qt.size(manageImg.width,manageImg.height) fillMode: Image.PreserveAspectFit smooth: true @@ -361,11 +340,11 @@ Rectangle { } Text { - anchors.horizontalCenter: manageButton.horizontalCenter + anchors.horizontalCenter: manageOrVideoButton.horizontalCenter y: 56 * virtualstudio.uiScale - text: manageable && connected ? "Video" : "Manage" + text: connected ? "Video" : "Manage" font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: manageable + visible: admin || connected color: textColour } } diff --git a/src/gui/headphones.svg b/src/gui/headphones.svg index fa3a213..8041f67 100644 --- a/src/gui/headphones.svg +++ b/src/gui/headphones.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/gui/help.svg b/src/gui/help.svg new file mode 100644 index 0000000..d2a8d02 --- /dev/null +++ b/src/gui/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/loud.svg b/src/gui/loud.svg new file mode 100644 index 0000000..b3aeb16 --- /dev/null +++ b/src/gui/loud.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/gui/mic.svg b/src/gui/mic.svg index 2568def..520b7f9 100644 --- a/src/gui/mic.svg +++ b/src/gui/mic.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/gui/micoff.svg b/src/gui/micoff.svg index e827d6f..8816538 100644 --- a/src/gui/micoff.svg +++ b/src/gui/micoff.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index ad5ebb2..cf0e61a 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -52,7 +53,7 @@ #include "../Meter.h" #include "../Reverb.h" -QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) +QJackTrip::QJackTrip(Settings* settings, bool suppressCommandlineWarning, QWidget* parent) : QMainWindow(parent) , m_ui(new Ui::QJackTrip) , m_netManager(new QNetworkAccessManager(this)) @@ -63,8 +64,6 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) , m_jackTripRunning(false) , m_isExiting(false) , m_exitSent(false) - , m_hasIPv4Reply(false) - , m_argc(argc) , m_hideWarning(false) { m_ui->setupUi(this); @@ -222,11 +221,21 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) m_ui->outClientsSpinBox->setEnabled(m_ui->outLimiterCheckBox->isChecked()); }); - connect(m_netManager.data(), &QNetworkAccessManager::finished, this, - &QJackTrip::receivedIP); - // Use the ipify API to find our external IP address. - m_netManager->get(QNetworkRequest(QUrl(QStringLiteral("https://api.ipify.org")))); - m_netManager->get(QNetworkRequest(QUrl(QStringLiteral("https://api6.ipify.org")))); + connect(m_ui->connectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() { + m_ui->connectScriptEdit->setEnabled(m_ui->connectScriptCheckBox->isChecked()); + m_ui->connectScriptBrowse->setEnabled(m_ui->connectScriptCheckBox->isChecked()); + }); + connect(m_ui->disconnectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() { + m_ui->disconnectScriptEdit->setEnabled( + m_ui->disconnectScriptCheckBox->isChecked()); + m_ui->disconnectScriptBrowse->setEnabled( + m_ui->disconnectScriptCheckBox->isChecked()); + }); + connect(m_ui->connectScriptBrowse, &QPushButton::clicked, this, + &QJackTrip::browseForFile); + connect(m_ui->disconnectScriptBrowse, &QPushButton::clicked, this, + &QJackTrip::browseForFile); + m_ui->statusBar->showMessage(QStringLiteral("JackTrip version ").append(gVersion)); // Set up our interface for the default Client run mode. @@ -283,20 +292,20 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) } #endif - // One of our arguments will always be --gui, so if that's the only one - // then we don't need to show the warning message. - if (((!gVerboseFlag && m_argc > 2) || m_argc > 3) && !suppressCommandlineWarning) { + migrateSettings(); + loadSettings(settings); + + // Display a warning about any ignored command line options. + if (settings->guiIgnoresArguments() && !suppressCommandlineWarning) { QMessageBox msgBox; msgBox.setText( - "The GUI version of JackTrip currently ignores any command line " - "options other than the verbose option (-V).\n\nThis may change in future."); + "You have supplied command line options that the GUI version of JackTrip " + "currently ignores. (Everything else will run as expected.)\n\nRun " + "\"jacktrip -h\" for more details."); msgBox.setWindowTitle(QStringLiteral("Command line options")); msgBox.exec(); } - migrateSettings(); - loadSettings(); - QVector labels; labels << m_ui->inFreeverbLabel << m_ui->inZitarevLabel << m_ui->outFreeverbLabel; std::srand(std::time(nullptr)); @@ -318,9 +327,8 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) // Check if Jack is actually available if (have_libjack() != 0) { #ifdef RT_AUDIO -#ifdef PSI - bool usingRtAudioAlready = m_ui->backendComboBox->currentIndex() == 1; -#endif // PSI + m_audioFallback = true; + m_usingRtAudioAlready = m_ui->backendComboBox->currentIndex() == 1; m_ui->backendComboBox->setCurrentIndex(1); m_ui->backendComboBox->setEnabled(false); m_ui->backendLabel->setEnabled(false); @@ -333,34 +341,6 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) m_ui->backendWarningLabel->setText( "JACK was not found. This means that only the RtAudio backend is available " "and that JackTrip cannot be run in hub server mode."); - -#ifdef PSI - QSettings settings; - settings.beginGroup(QStringLiteral("Audio")); - if (!settings.value(QStringLiteral("HideJackWarning"), false).toBool()) { - QCheckBox* dontBugMe = - new QCheckBox(QStringLiteral("Don't show this warning again")); - QMessageBox msgBox; - msgBox.setText( - "An installation of JACK was not found. JackTrip will still run using " - "a different audio backend (RtAudio) but some more advanced features, " - "like the ability to run your own hub server, will not be available." - "\n\n(If you install JACK at a later stage, these features will " - "automatically be re-enabled.)"); - msgBox.setWindowTitle(QStringLiteral("JACK Not Available")); - msgBox.setCheckBox(dontBugMe); - QObject::connect(dontBugMe, &QCheckBox::stateChanged, this, [=]() { - m_hideWarning = dontBugMe->isChecked(); - }); - msgBox.exec(); - if (m_hideWarning) { - settings.setValue(QStringLiteral("HideJackWarning"), true); - } - if (!usingRtAudioAlready) { - settings.setValue(QStringLiteral("UsingFallback"), true); - } - } - settings.endGroup(); } else { // If we've fallen back to RtAudio before and JACK is now installed, use JACK. QSettings settings; @@ -370,7 +350,6 @@ QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) settings.setValue(QStringLiteral("UsingFallback"), false); } settings.endGroup(); -#endif // PSI #else // RT_AUDIO QMessageBox msgBox; msgBox.setText( @@ -455,6 +434,45 @@ void QJackTrip::showEvent(QShowEvent* event) this->resize(QSize(this->size().height(), 600)); } settings.endGroup(); + + // Use the ipify API to find our external IP address. + connect(m_netManager.data(), &QNetworkAccessManager::finished, this, + &QJackTrip::receivedIP); + m_netManager->get(QNetworkRequest(QUrl(QStringLiteral("https://api.ipify.org")))); + m_netManager->get( + QNetworkRequest(QUrl(QStringLiteral("https://api6.ipify.org")))); + + // Also show our JACK not found warning if needed. +#ifdef RT_AUDIO + if (m_audioFallback) { + QSettings settings; + settings.beginGroup(QStringLiteral("Audio")); + if (!settings.value(QStringLiteral("HideJackWarning"), false).toBool()) { + QCheckBox* dontBugMe = + new QCheckBox(QStringLiteral("Don't show this warning again")); + QMessageBox msgBox; + msgBox.setText( + "An installation of JACK was not found. JackTrip will still run " + "using a different audio backend (RtAudio) but some more advanced " + "features, like the ability to run your own hub server, will not be " + "available.\n\n(If you install JACK at a later stage, these features " + "will automatically be re-enabled.)"); + msgBox.setWindowTitle(QStringLiteral("JACK Not Available")); + msgBox.setCheckBox(dontBugMe); + QObject::connect(dontBugMe, &QCheckBox::stateChanged, this, [=]() { + m_hideWarning = dontBugMe->isChecked(); + }); + msgBox.exec(); + if (m_hideWarning) { + settings.setValue(QStringLiteral("HideJackWarning"), true); + } + if (!m_usingRtAudioAlready) { + settings.setValue(QStringLiteral("UsingFallback"), true); + } + } + settings.endGroup(); + } +#endif // RT_AUDIO m_firstShow = false; } } @@ -483,6 +501,21 @@ void QJackTrip::processFinished() } else { m_jackTrip.reset(); } + + if (m_ui->disconnectScriptCheckBox->isChecked()) { + QStringList arguments = m_ui->disconnectScriptEdit->text().split( + QStringLiteral(" "), Qt::SkipEmptyParts); + if (!arguments.isEmpty()) { + QProcess disconnectScript; + disconnectScript.setProgram(arguments.takeFirst()); + disconnectScript.setWorkingDirectory(QDir::homePath()); + disconnectScript.setArguments(arguments); + disconnectScript.setStandardOutputFile(QProcess::nullDevice()); + disconnectScript.setStandardErrorFile(QProcess::nullDevice()); + disconnectScript.startDetached(); + } + } + if (m_isExiting) { m_exitSent = true; emit signalExit(); @@ -511,6 +544,19 @@ void QJackTrip::processError(const QString& errorMessage) void QJackTrip::receivedConnectionFromPeer() { m_ui->statusBar->showMessage(QStringLiteral("Received Connection from Peer!")); + if (m_ui->connectScriptCheckBox->isChecked()) { + QStringList arguments = m_ui->connectScriptEdit->text().split(QStringLiteral(" "), + Qt::SkipEmptyParts); + if (!arguments.isEmpty()) { + QProcess connectScript; + connectScript.setProgram(arguments.takeFirst()); + connectScript.setWorkingDirectory(QDir::homePath()); + connectScript.setArguments(arguments); + connectScript.setStandardOutputFile(QProcess::nullDevice()); + connectScript.setStandardErrorFile(QProcess::nullDevice()); + connectScript.startDetached(); + } + } } void QJackTrip::queueLengthChanged(int queueLength) @@ -560,6 +606,10 @@ void QJackTrip::chooseRunType(int index) if (index != -1) { m_ui->optionsTabWidget->removeTab(index); } + index = findTab(QStringLiteral("Scripting")); + if (index != -1) { + m_ui->optionsTabWidget->removeTab(index); + } authFilesChanged(); #ifdef RT_AUDIO index = findTab(QStringLiteral("Audio Backend")); @@ -576,6 +626,10 @@ void QJackTrip::chooseRunType(int index) if (findTab(QStringLiteral("Plugins")) == -1) { m_ui->optionsTabWidget->addTab(m_ui->pluginsTab, QStringLiteral("Plugins")); } + if (findTab(QStringLiteral("Scripting")) == -1) { + m_ui->optionsTabWidget->addTab(m_ui->scriptingTab, + QStringLiteral("Scripting")); + } #ifdef RT_AUDIO if (findTab(QStringLiteral("Audio Backend")) == -1) { m_ui->optionsTabWidget->insertTab(2, m_ui->backendTab, @@ -653,7 +707,13 @@ void QJackTrip::browseForFile() fileEdit = m_ui->keyEdit; } else { fileType = QLatin1String(""); - fileEdit = m_ui->credsEdit; + if (sender == m_ui->connectScriptBrowse) { + fileEdit = m_ui->connectScriptEdit; + } else if (sender == m_ui->disconnectScriptBrowse) { + fileEdit = m_ui->disconnectScriptEdit; + } else { + fileEdit = m_ui->credsEdit; + } } QString fileName = QFileDialog::getOpenFileName(this, QStringLiteral("Open File"), m_lastPath, fileType); @@ -667,6 +727,8 @@ void QJackTrip::browseForFile() void QJackTrip::receivedIP(QNetworkReply* reply) { QMutexLocker locker(&m_requestMutex); + m_replyCount++; + // Check whether we're dealing with our IPv4 or IPv6 request. if (reply->url().host().startsWith(QLatin1String("api6"))) { if (reply->error() == QNetworkReply::NoError) { @@ -677,28 +739,33 @@ void QJackTrip::receivedIP(QNetworkReply* reply) reply->deleteLater(); return; } - if (m_hasIPv4Reply) { - m_ui->ipLabel->setText(m_ui->ipLabel->text().append( - QStringLiteral("\n(IPv6: %1)").arg(m_IPv6Address))); - } - m_ui->ipLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); } } else { - if (reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::NoError) { + m_IPv4Address = QString(reply->readAll()); + } + } + + if (m_replyCount == 2) { + // Set our label if both replies have arrived. + if (m_IPv4Address.isEmpty() && m_IPv6Address.isEmpty()) { m_ui->ipLabel->setText( QStringLiteral("Unable to determine external IP address.")); + } else if (m_IPv4Address.isEmpty()) { + m_ui->ipLabel->setText( + QStringLiteral("External IPv6 address: ").append(m_IPv6Address)); + m_ui->ipLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); } else { - QByteArray address = reply->readAll(); m_ui->ipLabel->setText( - QStringLiteral("External IP address: ").append(address)); + QStringLiteral("External IP address: ").append(m_IPv4Address)); m_ui->ipLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + if (!m_IPv6Address.isEmpty()) { + m_ui->ipLabel->setText(m_ui->ipLabel->text().append( + QStringLiteral("\n(IPv6: %1)").arg(m_IPv6Address))); + } } - if (!m_IPv6Address.isEmpty()) { - m_ui->ipLabel->setText(m_ui->ipLabel->text().append( - QStringLiteral("\n(IPv6: %1)").arg(m_IPv6Address))); - } - m_hasIPv4Reply = true; } + reply->deleteLater(); } @@ -754,11 +821,8 @@ void QJackTrip::start() if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) { m_udpHub.reset(new UdpHubListener(m_ui->localPortSpinBox->value(), m_ui->basePortSpinBox->value())); - int hubConnectionMode = m_ui->autoPatchComboBox->currentIndex(); - if (hubConnectionMode > CLIENTFOFI) { - // Adjust for the RESERVEDMATRIX gap. - hubConnectionMode++; - } + int hubConnectionMode = hubModeFromPatchType( + static_cast(m_ui->autoPatchComboBox->currentIndex())); if (m_ui->patchServerCheckBox->isChecked()) { if (m_ui->autoPatchComboBox->currentIndex() == CLIENTFOFI) { hubConnectionMode = JackTrip::SERVFOFI; @@ -827,14 +891,14 @@ void QJackTrip::start() jackTripMode = JackTrip::CLIENTTOPINGSERVER; } - m_jackTrip.reset(new JackTrip(jackTripMode, JackTrip::UDP, - m_ui->channelSendSpinBox->value(), - m_ui->channelRecvSpinBox->value(), + m_jackTrip.reset(new JackTrip( + jackTripMode, JackTrip::UDP, 0, m_ui->channelSendSpinBox->value(), 0, + m_ui->channelRecvSpinBox->value(), AudioInterface::MIX_UNSET, #ifdef WAIR // wair - 0, + 0, #endif // endwhere - m_ui->queueLengthSpinBox->value(), - m_ui->redundancySpinBox->value(), resolution)); + m_ui->queueLengthSpinBox->value(), m_ui->redundancySpinBox->value(), + resolution)); m_jackTrip->setConnectDefaultAudioPorts( m_ui->connectAudioCheckBox->isChecked()); if (m_ui->zeroCheckBox->isChecked()) { @@ -1003,13 +1067,13 @@ void QJackTrip::exit() } } -void QJackTrip::updatedInputMeasurements(const QVector valuesInDb) +void QJackTrip::updatedInputMeasurements(const float* valuesInDb, int numChannels) { for (int i = 0; i < m_inputMeters.count(); i++) { // Determine decibel reading qreal dB = m_meterMin; - if (i < valuesInDb.size()) { - dB = std::max(m_meterMin, valuesInDb.at(i)); + if (i < numChannels) { + dB = std::max(m_meterMin, valuesInDb[i]); } // Produce a normalized value from 0 to 1 @@ -1018,13 +1082,13 @@ void QJackTrip::updatedInputMeasurements(const QVector valuesInDb) } } -void QJackTrip::updatedOutputMeasurements(const QVector valuesInDb) +void QJackTrip::updatedOutputMeasurements(const float* valuesInDb, int numChannels) { for (int i = 0; i < m_outputMeters.count(); i++) { // Determine decibel reading qreal dB = m_meterMin; - if (i < valuesInDb.size()) { - dB = std::max(m_meterMin, valuesInDb.at(i)); + if (i < numChannels) { + dB = std::max(m_meterMin, valuesInDb[i]); } // Produce a normalized value from 0 to 1 @@ -1085,8 +1149,6 @@ void QJackTrip::advancedOptionsForHubServer(bool isHubServer) m_ui->clientNameEdit->setVisible(!isHubServer); m_ui->redundancyLabel->setVisible(!isHubServer); m_ui->redundancySpinBox->setVisible(!isHubServer); - m_ui->resolutionLabel->setVisible(!isHubServer); - m_ui->resolutionComboBox->setVisible(!isHubServer); m_ui->connectAudioCheckBox->setVisible(!isHubServer); m_ui->basePortLabel->setVisible(isHubServer); m_ui->basePortSpinBox->setVisible(isHubServer); @@ -1120,58 +1182,22 @@ void QJackTrip::migrateSettings() settings.setValue(QStringLiteral("Migrated"), true); } -void QJackTrip::loadSettings() +void QJackTrip::loadSettings(Settings* cliSettings) { QSettings settings; - m_ui->typeComboBox->setCurrentIndex( - settings.value(QStringLiteral("RunMode"), 2).toInt()); + bool useCommandLine = false; + if (cliSettings) { + useCommandLine = cliSettings->isModeSet(); + } - // Migrate to separate send and receive channel numbers. + // Migrate to separate send and receive channel numbers first if needed int oldChannelSetting = settings.value(QStringLiteral("Channels"), -1).toInt(); if (oldChannelSetting != -1) { - m_ui->channelSendSpinBox->setValue(oldChannelSetting); - m_ui->channelRecvSpinBox->setValue(oldChannelSetting); + settings.setValue(QStringLiteral("ChannelsSend"), oldChannelSetting); + settings.setValue(QStringLiteral("ChannelsRecv"), oldChannelSetting); settings.remove(QStringLiteral("Channels")); - } else { - m_ui->channelSendSpinBox->setValue( - settings.value(QStringLiteral("ChannelsSend"), gDefaultNumInChannels) - .toInt()); - m_ui->channelRecvSpinBox->setValue( - settings.value(QStringLiteral("ChannelsRecv"), gDefaultNumOutChannels) - .toInt()); } - m_ui->autoPatchComboBox->setCurrentIndex( - settings.value(QStringLiteral("AutoPatchMode"), 0).toInt()); - m_ui->patchServerCheckBox->setChecked( - settings.value(QStringLiteral("PatchIncludesServer"), false).toBool()); - m_ui->upmixCheckBox->setChecked( - settings.value(QStringLiteral("StereoUpmix"), false).toBool()); - m_ui->zeroCheckBox->setChecked( - settings.value(QStringLiteral("ZeroUnderrun"), false).toBool()); - m_ui->timeoutCheckBox->setChecked( - settings.value(QStringLiteral("Timeout"), false).toBool()); - m_ui->clientNameEdit->setText( - settings.value(QStringLiteral("ClientName"), "").toString()); - m_ui->remoteNameEdit->setText( - settings.value(QStringLiteral("RemoteName"), "").toString()); - m_ui->localPortSpinBox->setValue( - settings.value(QStringLiteral("LocalPort"), gDefaultPort).toInt()); - m_ui->remotePortSpinBox->setValue( - settings.value(QStringLiteral("RemotePort"), gDefaultPort).toInt()); - m_ui->basePortSpinBox->setValue( - settings.value(QStringLiteral("BasePort"), 61002).toInt()); - m_ui->queueLengthSpinBox->setValue( - settings.value(QStringLiteral("QueueLength"), gDefaultQueueLength).toInt()); - m_ui->redundancySpinBox->setValue( - settings.value(QStringLiteral("Redundancy"), gDefaultRedundancy).toInt()); - m_ui->resolutionComboBox->setCurrentIndex( - settings.value(QStringLiteral("Resolution"), 1).toInt()); - m_ui->connectAudioCheckBox->setChecked( - settings.value(QStringLiteral("ConnectAudio"), true).toBool()); - m_ui->realTimeCheckBox->setChecked( - settings.value(QStringLiteral("RTNetworking"), true).toBool()); - // This may have been set by the command line, so don't overwrite if that's the case. m_ui->verboseCheckBox->setChecked( gVerboseFlag || settings.value(QStringLiteral("Debug"), 0).toBool()); m_lastPath = settings.value(QStringLiteral("LastPath"), QDir::homePath()).toString(); @@ -1186,9 +1212,217 @@ void QJackTrip::loadSettings() } settings.endGroup(); // Need to get this here so it isn't overwritten by the previous section. - m_ui->addressComboBox->setCurrentText( - settings.value(QStringLiteral("LastAddress"), "").toString()); + if (useCommandLine && !cliSettings->getPeerAddress().isEmpty()) { + m_ui->addressComboBox->setCurrentText(cliSettings->getPeerAddress()); + } else { + m_ui->addressComboBox->setCurrentText( + settings.value(QStringLiteral("LastAddress"), "").toString()); + } + + if (useCommandLine) { + JackTrip::jacktripModeT mode = cliSettings->getJackTripMode(); + if (mode == JackTrip::CLIENT) { + m_ui->typeComboBox->setCurrentIndex(P2P_CLIENT); + } else if (mode == JackTrip::SERVER) { + m_ui->typeComboBox->setCurrentIndex(P2P_SERVER); + } else if (mode == JackTrip::CLIENTTOPINGSERVER) { + m_ui->typeComboBox->setCurrentIndex(HUB_CLIENT); + } else { + m_ui->typeComboBox->setCurrentIndex(HUB_SERVER); + } + m_ui->channelSendSpinBox->setValue(cliSettings->getNumAudioInputChans()); + m_ui->channelRecvSpinBox->setValue(cliSettings->getNumAudioOutputChans()); + + unsigned int patchMode = cliSettings->getHubConnectionMode(); + if (patchMode == JackTrip::SERVERTOCLIENT) { + m_ui->autoPatchComboBox->setCurrentIndex(SERVERTOCLIENT); + } else if (patchMode == JackTrip::CLIENTECHO) { + m_ui->autoPatchComboBox->setCurrentIndex(CLIENTECHO); + } else if (patchMode == JackTrip::CLIENTFOFI) { + m_ui->autoPatchComboBox->setCurrentIndex(CLIENTFOFI); + } else if (patchMode == JackTrip::FULLMIX) { + m_ui->autoPatchComboBox->setCurrentIndex(FULLMIX); + } else { + // Accomodate for the fact that the GUI doesn't support the reserved patching + // mode by disabling patching if selected. + m_ui->autoPatchComboBox->setCurrentIndex(NOAUTO); + } + + m_ui->patchServerCheckBox->setChecked(cliSettings->getPatchServerAudio()); + m_ui->upmixCheckBox->setChecked(cliSettings->getPatchServerAudio()); + m_ui->zeroCheckBox->setChecked(cliSettings->getUnderrunMode() == JackTrip::ZEROS); + m_ui->timeoutCheckBox->setChecked(cliSettings->getStopOnTimeout()); + m_ui->clientNameEdit->setText(cliSettings->getClientName()); + m_ui->remoteNameEdit->setText(cliSettings->getRemoteClientName()); + m_ui->localPortSpinBox->setValue(cliSettings->getBindPort()); + m_ui->remotePortSpinBox->setValue(cliSettings->getPeerPort()); + int basePort = cliSettings->getServerUdpPort(); + if (basePort == 0) { + // TODO: This currently mirrors the behaviour seen in UdpHubListener.cpp, but + // I'm not sure it's particularly intuitive. It makes sense if the bind port + // was changed using the offset flag -o but not if it was changed using -B. + // These two cases are not currently distinguished between. + basePort = 61002 + cliSettings->getBindPort() - gDefaultPort; + } + m_ui->basePortSpinBox->setValue(basePort); + int queueLength = cliSettings->getQueueLength(); + m_ui->queueLengthSpinBox->setValue( + queueLength > 0 + ? queueLength + : settings.value(QStringLiteral("QueueLength"), gDefaultQueueLength) + .toInt()); + m_ui->redundancySpinBox->setValue(cliSettings->getRedundancy()); + AudioInterface::audioBitResolutionT resolution = + cliSettings->getAudioBitResolution(); + if (resolution == AudioInterface::BIT8) { + m_ui->resolutionComboBox->setCurrentIndex(0); + } else if (resolution == AudioInterface::BIT16) { + m_ui->resolutionComboBox->setCurrentIndex(1); + } else if (resolution == AudioInterface::BIT24) { + m_ui->resolutionComboBox->setCurrentIndex(2); + } else { + m_ui->resolutionComboBox->setCurrentIndex(3); + } + m_ui->connectAudioCheckBox->setChecked( + cliSettings->getConnectDefaultAudioPorts()); + m_ui->realTimeCheckBox->setChecked(cliSettings->getUseRtUdpPriority()); + + m_ui->requireAuthCheckBox->setChecked(cliSettings->getUseAuthentication()); + m_ui->authCheckBox->setChecked(cliSettings->getUseAuthentication()); + m_ui->certEdit->setText(cliSettings->getCertFile()); + m_ui->keyEdit->setText(cliSettings->getKeyFile()); + m_ui->credsEdit->setText(cliSettings->getCredsFile()); + m_ui->usernameEdit->setText(cliSettings->getUsername()); + m_ui->passwordEdit->setText(cliSettings->getPassword()); + + settings.beginGroup(QStringLiteral("JitterBuffer")); + settings.setValue(QStringLiteral("JitterAnnounce"), true); + int bufferStrategy = cliSettings->getBufferStrategy(); + m_ui->jitterCheckBox->setChecked(bufferStrategy > 0); + m_ui->broadcastCheckBox->setChecked(cliSettings->getBroadCastQueue() > 0); + m_ui->broadcastQueueSpinBox->setValue( + cliSettings->getBroadCastQueue() > 0 + ? cliSettings->getBroadCastQueue() + : settings + .value(QStringLiteral("BroadcastLength"), gDefaultQueueLength * 2) + .toInt()); + if (bufferStrategy > 0) { + m_ui->bufferStrategyComboBox->setCurrentIndex(bufferStrategy - 1); + } else { + m_ui->bufferStrategyComboBox->setCurrentIndex( + settings.value(QStringLiteral("Strategy"), 1).toInt() - 1); + } + m_ui->autoQueueCheckBox->setChecked(queueLength < 0); + m_ui->autoQueueSpinBox->setValue( + queueLength < 0 + ? std::abs(queueLength) + : settings.value(QStringLiteral("TuningParameter"), 500).toInt()); + settings.endGroup(); + } else { + m_ui->typeComboBox->setCurrentIndex( + settings.value(QStringLiteral("RunMode"), 2).toInt()); + m_ui->zeroCheckBox->setChecked( + settings.value(QStringLiteral("ZeroUnderrun"), false).toBool()); + m_ui->localPortSpinBox->setValue( + settings.value(QStringLiteral("LocalPort"), gDefaultPort).toInt()); + m_ui->queueLengthSpinBox->setValue( + settings.value(QStringLiteral("QueueLength"), gDefaultQueueLength).toInt()); + m_ui->resolutionComboBox->setCurrentIndex( + settings.value(QStringLiteral("Resolution"), 1).toInt()); + m_ui->realTimeCheckBox->setChecked( + settings.value(QStringLiteral("RTNetworking"), true).toBool()); + + settings.beginGroup(QStringLiteral("JitterBuffer")); + bool jitterAnnounce = + settings.value(QStringLiteral("JitterAnnounce"), false).toBool(); + if (!jitterAnnounce + && !settings.value(QStringLiteral("Enabled"), true).toBool()) { + QMessageBox msgBox; + msgBox.setText( + "From this build onwards, the new jitter buffer is being enabled by " + "default. " + "You can turn it off in the Jitter Buffer settings tab."); + msgBox.setWindowTitle(QStringLiteral("Jitter Buffer")); + msgBox.exec(); + settings.setValue(QStringLiteral("Enabled"), true); + } + settings.setValue(QStringLiteral("JitterAnnounce"), true); + m_ui->jitterCheckBox->setChecked( + settings.value(QStringLiteral("Enabled"), true).toBool()); + m_ui->broadcastCheckBox->setChecked( + settings.value(QStringLiteral("Broadcast"), false).toBool()); + m_ui->broadcastQueueSpinBox->setValue( + settings.value(QStringLiteral("BroadcastLength"), gDefaultQueueLength * 2) + .toInt()); + m_ui->bufferStrategyComboBox->setCurrentIndex( + settings.value(QStringLiteral("Strategy"), 1).toInt() - 1); + m_ui->autoQueueCheckBox->setChecked( + settings.value(QStringLiteral("AutoQueue"), true).toBool()); + m_ui->autoQueueSpinBox->setValue( + settings.value(QStringLiteral("TuningParameter"), 500).toInt()); + settings.endGroup(); + } + + // These settings may need to be loaded even if we were using our command line. + // (This depends on the mode that was selected.) + if (!useCommandLine || m_ui->typeComboBox->currentIndex() == HUB_SERVER) { + m_ui->channelSendSpinBox->setValue( + settings.value(QStringLiteral("ChannelsSend"), gDefaultNumInChannels) + .toInt()); + m_ui->channelRecvSpinBox->setValue( + settings.value(QStringLiteral("ChannelsRecv"), gDefaultNumOutChannels) + .toInt()); + m_ui->timeoutCheckBox->setChecked( + settings.value(QStringLiteral("Timeout"), false).toBool()); + m_ui->clientNameEdit->setText( + settings.value(QStringLiteral("ClientName"), "").toString()); + m_ui->redundancySpinBox->setValue( + settings.value(QStringLiteral("Redundancy"), gDefaultRedundancy).toInt()); + m_ui->connectAudioCheckBox->setChecked( + settings.value(QStringLiteral("ConnectAudio"), true).toBool()); + } + if (!useCommandLine || !(m_ui->typeComboBox->currentIndex() == HUB_SERVER)) { + m_ui->autoPatchComboBox->setCurrentIndex( + settings.value(QStringLiteral("AutoPatchMode"), 0).toInt()); + m_ui->patchServerCheckBox->setChecked( + settings.value(QStringLiteral("PatchIncludesServer"), false).toBool()); + m_ui->upmixCheckBox->setChecked( + settings.value(QStringLiteral("StereoUpmix"), false).toBool()); + m_ui->basePortSpinBox->setValue( + settings.value(QStringLiteral("BasePort"), 61002).toInt()); + } + if (!useCommandLine || !(m_ui->typeComboBox->currentIndex() == HUB_CLIENT)) { + m_ui->remoteNameEdit->setText( + settings.value(QStringLiteral("RemoteName"), "").toString()); + } + if (!useCommandLine + || !(m_ui->typeComboBox->currentIndex() == HUB_CLIENT + || m_ui->typeComboBox->currentIndex() == P2P_CLIENT)) { + m_ui->remotePortSpinBox->setValue( + settings.value(QStringLiteral("RemotePort"), gDefaultPort).toInt()); + } + settings.beginGroup(QStringLiteral("Auth")); + if (!useCommandLine || !(m_ui->typeComboBox->currentIndex() == HUB_SERVER)) { + m_ui->requireAuthCheckBox->setChecked( + settings.value(QStringLiteral("Require"), false).toBool()); + m_ui->certEdit->setText( + settings.value(QStringLiteral("CertFile"), "").toString()); + m_ui->keyEdit->setText(settings.value(QStringLiteral("KeyFile"), "").toString()); + m_ui->credsEdit->setText( + settings.value(QStringLiteral("CredsFile"), "").toString()); + } + if (!useCommandLine || !(m_ui->typeComboBox->currentIndex() == HUB_CLIENT)) { + m_ui->authCheckBox->setChecked( + settings.value(QStringLiteral("Use"), false).toBool()); + m_ui->usernameEdit->setText( + settings.value(QStringLiteral("Username"), "").toString()); + m_ui->passwordEdit->setText(""); + } + settings.endGroup(); + + // Settings from this point onwards are currently read only from the previously stored + // values and not from the commmand line. #ifdef RT_AUDIO settings.beginGroup(QStringLiteral("Audio")); m_ui->backendComboBox->setCurrentIndex( @@ -1211,17 +1445,6 @@ void QJackTrip::loadSettings() settings.endGroup(); #endif - settings.beginGroup(QStringLiteral("Auth")); - m_ui->requireAuthCheckBox->setChecked( - settings.value(QStringLiteral("Require"), false).toBool()); - m_ui->certEdit->setText(settings.value(QStringLiteral("CertFile"), "").toString()); - m_ui->keyEdit->setText(settings.value(QStringLiteral("KeyFile"), "").toString()); - m_ui->credsEdit->setText(settings.value(QStringLiteral("CredsFile"), "").toString()); - m_ui->authCheckBox->setChecked(settings.value(QStringLiteral("Use"), false).toBool()); - m_ui->usernameEdit->setText( - settings.value(QStringLiteral("Username"), "").toString()); - settings.endGroup(); - settings.beginGroup(QStringLiteral("IOStats")); m_ui->ioStatsCheckBox->setChecked( settings.value(QStringLiteral("Display"), false).toBool()); @@ -1229,34 +1452,6 @@ void QJackTrip::loadSettings() settings.value(QStringLiteral("ReportingInterval"), 1).toInt()); settings.endGroup(); - settings.beginGroup(QStringLiteral("JitterBuffer")); - bool jitterAnnounce = - settings.value(QStringLiteral("JitterAnnounce"), false).toBool(); - if (!jitterAnnounce && !settings.value(QStringLiteral("Enabled"), true).toBool()) { - QMessageBox msgBox; - msgBox.setText( - "From this build onwards, the new jitter buffer is being enabled by default. " - "You can turn it off in the Jitter Buffer settings tab."); - msgBox.setWindowTitle(QStringLiteral("Jitter Buffer")); - msgBox.exec(); - settings.setValue(QStringLiteral("Enabled"), true); - } - settings.setValue(QStringLiteral("JitterAnnounce"), true); - m_ui->jitterCheckBox->setChecked( - settings.value(QStringLiteral("Enabled"), true).toBool()); - m_ui->broadcastCheckBox->setChecked( - settings.value(QStringLiteral("Broadcast"), false).toBool()); - m_ui->broadcastQueueSpinBox->setValue( - settings.value(QStringLiteral("BroadcastLength"), gDefaultQueueLength * 2) - .toInt()); - m_ui->bufferStrategyComboBox->setCurrentIndex( - settings.value(QStringLiteral("Strategy"), 1).toInt() - 1); - m_ui->autoQueueCheckBox->setChecked( - settings.value(QStringLiteral("AutoQueue"), true).toBool()); - m_ui->autoQueueSpinBox->setValue( - settings.value(QStringLiteral("TuningParameter"), 500).toInt()); - settings.endGroup(); - settings.beginGroup(QStringLiteral("InPlugins")); m_ui->inFreeverbCheckBox->setChecked( settings.value(QStringLiteral("Freeverb"), false).toBool()); @@ -1288,6 +1483,17 @@ void QJackTrip::loadSettings() m_ui->outClientsSpinBox->setValue( settings.value(QStringLiteral("Clients"), 1).toInt()); settings.endGroup(); + + settings.beginGroup(QStringLiteral("Scripting")); + m_ui->connectScriptCheckBox->setChecked( + settings.value(QStringLiteral("ConnectEnabled"), false).toBool()); + m_ui->connectScriptEdit->setText( + settings.value(QStringLiteral("ConnectScript"), "").toString()); + m_ui->disconnectScriptCheckBox->setChecked( + settings.value(QStringLiteral("DisconnectEnabled"), false).toBool()); + m_ui->disconnectScriptEdit->setText( + settings.value(QStringLiteral("DisconnectScript"), "").toString()); + settings.endGroup(); } void QJackTrip::saveSettings() @@ -1392,6 +1598,16 @@ void QJackTrip::saveSettings() settings.setValue(QStringLiteral("Clients"), m_ui->outClientsSpinBox->value()); settings.endGroup(); + settings.beginGroup(QStringLiteral("Scripting")); + settings.setValue(QStringLiteral("ConnectEnabled"), + m_ui->connectScriptCheckBox->isChecked()); + settings.setValue(QStringLiteral("ConnectScript"), m_ui->connectScriptEdit->text()); + settings.setValue(QStringLiteral("DisconnectEnabled"), + m_ui->disconnectScriptCheckBox->isChecked()); + settings.setValue(QStringLiteral("DisconnectScript"), + m_ui->disconnectScriptEdit->text()); + settings.endGroup(); + settings.beginGroup(QStringLiteral("Window")); settings.setValue(QStringLiteral("Geometry"), saveGeometry()); settings.endGroup(); @@ -1526,11 +1742,8 @@ QString QJackTrip::commandLineFromCurrentOptions() } if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) { - int hubConnectionMode = m_ui->autoPatchComboBox->currentIndex(); - if (hubConnectionMode > CLIENTFOFI) { - // Adjust for the RESERVEDMATRIX gap. - hubConnectionMode++; - } + int hubConnectionMode = hubModeFromPatchType( + static_cast(m_ui->autoPatchComboBox->currentIndex())); if (hubConnectionMode > 0) { commandLine.append(QStringLiteral(" -p %1").arg(hubConnectionMode)); } @@ -1790,6 +2003,22 @@ void QJackTrip::showCommandLineMessageBox() msgBox.exec(); } +JackTrip::hubConnectionModeT QJackTrip::hubModeFromPatchType( + QJackTrip::patchTypeT patchType) +{ + if (patchType == SERVERTOCLIENT) { + return JackTrip::SERVERTOCLIENT; + } else if (patchType == CLIENTECHO) { + return JackTrip::CLIENTECHO; + } else if (patchType == CLIENTFOFI) { + return JackTrip::CLIENTFOFI; + } else if (patchType == FULLMIX) { + return JackTrip::FULLMIX; + } else { + return JackTrip::NOAUTO; + } +} + QJackTrip::~QJackTrip() { // Restore cout. (Stops a crash on exit.) diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h index ff0a75e..db444b6 100644 --- a/src/gui/qjacktrip.h +++ b/src/gui/qjacktrip.h @@ -39,6 +39,7 @@ #include #include "../JackTrip.h" +#include "../Settings.h" #include "../UdpHubListener.h" #include "messageDialog.h" #include "vuMeter.h" @@ -65,7 +66,7 @@ class QJackTrip : public QMainWindow Q_OBJECT public: - explicit QJackTrip(int argc = 0, bool suppressCommandlineWarning = false, + explicit QJackTrip(Settings* settings, bool suppressCommandlineWarning = false, QWidget* parent = nullptr); ~QJackTrip() override; @@ -97,8 +98,8 @@ class QJackTrip : public QMainWindow void start(); void stop(); void exit(); - void updatedInputMeasurements(const QVector valuesInDb); - void updatedOutputMeasurements(const QVector valuesInDb); + void updatedInputMeasurements(const float* valuesInDb, int numChannels); + void updatedOutputMeasurements(const float* valuesInDb, int numChannels); #ifndef NO_VS void virtualStudioMode(); #endif @@ -111,7 +112,7 @@ class QJackTrip : public QMainWindow void enableUi(bool enabled); void advancedOptionsForHubServer(bool isHubServer); void migrateSettings(); - void loadSettings(); + void loadSettings(Settings* cliSettings = nullptr); void saveSettings(); #ifdef RT_AUDIO @@ -125,6 +126,8 @@ class QJackTrip : public QMainWindow QString commandLineFromCurrentOptions(); void showCommandLineMessageBox(); + JackTrip::hubConnectionModeT hubModeFromPatchType(patchTypeT patchType); + QScopedPointer m_ui; QScopedPointer m_udpHub; QScopedPointer m_jackTrip; @@ -149,13 +152,15 @@ class QJackTrip : public QMainWindow QMutex m_requestMutex; QString m_IPv6Address; - bool m_hasIPv4Reply; + QString m_IPv4Address; + int m_replyCount = 0; QString m_lastPath; QLabel m_autoQueueIndicator; - int m_argc; bool m_hideWarning; - bool m_firstShow = true; + bool m_audioFallback = false; + bool m_usingRtAudioAlready = false; + bool m_firstShow = true; #ifndef NO_VS QSharedPointer m_vs; diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index 98c85bb..34117a0 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -10,6 +10,7 @@ Login.qml Studio.qml Browse.qml + AudioSettings.qml Settings.qml Meter.qml Connected.qml @@ -29,6 +30,10 @@ cog.svg mic.svg micoff.svg + help.svg + quiet.svg + loud.svg + refresh.svg ethernet.png ohno.png headphones.svg diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui index 11a8fb2..b562774 100644 --- a/src/gui/qjacktrip.ui +++ b/src/gui/qjacktrip.ui @@ -1763,6 +1763,74 @@ and wetness is the essence of beauty. + + + Scripting + + + + + + false + + + + + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + Browse + + + + + + + Execute script on &connection + + + + + + + Execute script on &disconnection + + + + + + + false + + + Browse + + + + + @@ -1905,6 +1973,12 @@ To connect to a hub server you need to run as a hub client. outCompressorCheckBox outLimiterCheckBox outClientsSpinBox + connectScriptCheckBox + connectScriptEdit + connectScriptBrowse + disconnectScriptCheckBox + disconnectScriptEdit + disconnectScriptBrowse diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc index 5fe08a8..179c85a 100644 --- a/src/gui/qjacktrip_novs.qrc +++ b/src/gui/qjacktrip_novs.qrc @@ -1,7 +1,7 @@ - alt/about@2x.png - alt/about.png - alt/icon.png + about@2x.png + about.png + icon.png diff --git a/src/gui/quiet.svg b/src/gui/quiet.svg new file mode 100644 index 0000000..b2ea070 --- /dev/null +++ b/src/gui/quiet.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/gui/refresh.svg b/src/gui/refresh.svg new file mode 100644 index 0000000..d2c5a92 --- /dev/null +++ b/src/gui/refresh.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/gui/share.svg b/src/gui/share.svg index 98a1ea2..77f6e6c 100644 --- a/src/gui/share.svg +++ b/src/gui/share.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp index 29bf756..fb168ef 100644 --- a/src/gui/virtualstudio.cpp +++ b/src/gui/virtualstudio.cpp @@ -31,7 +31,7 @@ /** * \file virtualstudio.cpp - * \author Aaron Wyatt + * \author Matt Horton, based on code by Aaron Wyatt * \date March 2022 */ @@ -113,9 +113,49 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_inMuted = settings.value(QStringLiteral("InMuted"), false).toBool(); m_outMuted = settings.value(QStringLiteral("OutMuted"), false).toBool(); #ifdef RT_AUDIO - m_useRtAudio = settings.value(QStringLiteral("Backend"), 1).toInt() == 1; - m_inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); - m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); + m_useRtAudio = settings.value(QStringLiteral("Backend"), 1).toInt() == 1; + m_inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); + m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); + + if (m_inputDevice == QStringLiteral("(default)")) { + m_inputDevice = ""; + } + if (m_outputDevice == QStringLiteral("(default)")) { + m_outputDevice = ""; + } + + // use default base channel 0, if the setting does not exist + m_baseInputChannel = settings.value(QStringLiteral("BaseInputChannel"), 0).toInt(); + m_baseOutputChannel = settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt(); + + // Handle migration scenarios. Assume this is a new user + // if we have m_inputDevice == "" and m_outputDevice == "" + if (m_inputDevice == "" && m_outputDevice == "") { + // for fresh installs, use mono by default + m_numInputChannels = + settings.value(QStringLiteral("NumInputChannels"), 1).toInt(); + m_inputMixMode = settings + .value(QStringLiteral("InputMixMode"), + static_cast(AudioInterface::MONO)) + .toInt(); + + // use 2 channels for output + m_numOutputChannels = + settings.value(QStringLiteral("NumOutputChannels"), 2).toInt(); + } else { + // existing installs - keep using stereo + m_numInputChannels = + settings.value(QStringLiteral("NumInputChannels"), 2).toInt(); + m_inputMixMode = settings + .value(QStringLiteral("InputMixMode"), + static_cast(AudioInterface::STEREO)) + .toInt(); + + // use 2 channels for output + m_numOutputChannels = + settings.value(QStringLiteral("NumOutputChannels"), 2).toInt(); + } + m_bufferSize = settings.value(QStringLiteral("BufferSize"), 128).toInt(); m_previousBuffer = m_bufferSize; refreshDevices(); @@ -136,6 +176,41 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_view.engine()->rootContext()->setContextProperty( QStringLiteral("outputComboModel"), QVariant::fromValue(QStringList(QLatin1String("")))); + + QJsonObject inputMixModeComboElement = QJsonObject(); + inputMixModeComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputMixModeComboModel"), + QVariant::fromValue( + QVariant(QVariantList() << QVariant(QJsonValue(inputMixModeComboElement))))); + + QJsonObject inputChannelsComboElement = QJsonObject(); + inputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1")); + inputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + inputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(1).toInt()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputChannelsComboModel"), + QVariant::fromValue( + QVariant(QVariantList() << QVariant(QJsonValue(inputChannelsComboElement))))); + + QJsonObject outputChannelsComboElement = QJsonObject(); + outputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1 & 2")); + outputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + outputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(2).toInt()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputChannelsComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(outputChannelsComboElement))))); + #endif m_bufferStrategy = settings.value(QStringLiteral("BufferStrategy"), 0).toInt(); settings.endGroup(); @@ -204,7 +279,7 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_view.setMinimumSize(QSize(594, 519)); // m_view.setMaximumSize(QSize(696, 577)); m_view.setResizeMode(QQuickView::SizeRootObjectToView); - m_view.resize(696 * m_uiScale, 577 * m_uiScale); + m_view.resize(696 * m_uiScale, 676 * m_uiScale); // Connect our timers connect(&m_retryPeriodTimer, &QTimer::timeout, this, &VirtualStudio::endRetryPeriod); @@ -249,7 +324,10 @@ void VirtualStudio::show() if (m_checkSsl) { // Check our available SSL version QString sslVersion = QSslSocket::sslLibraryVersionString(); - std::cout << "SSL Library: " << sslVersion.toStdString() << std::endl; + // Important: this needs to be output with qDebug rather than to std::cout + // otherwise it may get passed to an existing JackTrip instance in place of our + // deeplink. (Need to find the root cause of this.) + qDebug() << "SSL Library: " << sslVersion; if (sslVersion.isEmpty()) { QMessageBox msgBox; msgBox.setText( @@ -273,6 +351,11 @@ void VirtualStudio::raiseToTop() m_view.requestActivate(); // Raise to top } +bool VirtualStudio::vsModeActive() +{ + return m_vsModeActive; +} + bool VirtualStudio::showFirstRun() { return m_showFirstRun; @@ -318,93 +401,140 @@ void VirtualStudio::setAudioBackend(const QString& backend) emit audioBackendChanged(m_useRtAudio); } -int VirtualStudio::inputDevice() +QString VirtualStudio::inputDevice() { #ifdef RT_AUDIO - if (m_useRtAudio) { - QStringList filteredInputDeviceList; - for (int i = 0; i < m_inputDeviceList.size(); i++) { - if (m_inputDeviceList.at(i) != "(default)") { - filteredInputDeviceList += m_inputDeviceList.at(i); - } - } - - int index = filteredInputDeviceList.indexOf(m_inputDevice); - return index >= 0 ? index : 0; - } + return m_inputDevice; #endif - return 0; + return QStringLiteral(""); } -void VirtualStudio::setInputDevice([[maybe_unused]] int device) +void VirtualStudio::setInputDevice([[maybe_unused]] const QString& device) { if (!m_useRtAudio) { return; } #ifdef RT_AUDIO - std::cout << "Setting Input Device: " << device << std::endl; - QStringList filteredInputDeviceList; - for (int i = 0; i < m_inputDeviceList.size(); i++) { - if (m_inputDeviceList.at(i) != "(default)") { - filteredInputDeviceList += m_inputDeviceList.at(i); - } - } - - m_inputDevice = filteredInputDeviceList.at(device); + m_inputDevice = device; emit inputDeviceChanged(m_inputDevice, false); emit inputDeviceSelected(m_inputDevice); #endif } -int VirtualStudio::outputDevice() -{ #ifdef RT_AUDIO +int VirtualStudio::baseInputChannel() +{ if (m_useRtAudio) { - QStringList filteredOutputDeviceList; - for (int i = 0; i < m_outputDeviceList.size(); i++) { - if (m_outputDeviceList.at(i) != "(default)") { - filteredOutputDeviceList += m_outputDeviceList.at(i); - } - } + return m_baseInputChannel; + } + return 0; +} - int index = filteredOutputDeviceList.indexOf(m_outputDevice); - return index >= 0 ? index : 0; +void VirtualStudio::setBaseInputChannel([[maybe_unused]] int baseChannel) +{ + if (!m_useRtAudio) { + return; + } + m_baseInputChannel = baseChannel; + emit baseInputChannelChanged(m_baseInputChannel, true); +} + +int VirtualStudio::numInputChannels() +{ + if (m_useRtAudio) { + return m_numInputChannels; } -#endif return 0; } -void VirtualStudio::setOutputDevice([[maybe_unused]] int device) +void VirtualStudio::setNumInputChannels([[maybe_unused]] int numChannels) { if (!m_useRtAudio) { return; } -#ifdef RT_AUDIO - QStringList filteredOutputDeviceList; - for (int i = 0; i < m_outputDeviceList.size(); i++) { - if (m_outputDeviceList.at(i) != "(default)") { - filteredOutputDeviceList += m_outputDeviceList.at(i); - } + m_numInputChannels = numChannels; + emit numInputChannelsChanged(m_numInputChannels, true); +} + +void VirtualStudio::setInputMixMode(int mode) +{ + if (!m_useRtAudio) { + return; } + m_inputMixMode = mode; + emit inputMixModeChanged(m_inputMixMode, true); +} + +int VirtualStudio::inputMixMode() +{ + if (!m_useRtAudio) { + return -1; + } + return m_inputMixMode; +} +#endif - m_outputDevice = filteredOutputDeviceList.at(device); +QString VirtualStudio::outputDevice() +{ +#ifdef RT_AUDIO + return m_outputDevice; +#endif + return QStringLiteral(""); +} + +void VirtualStudio::setOutputDevice([[maybe_unused]] const QString& device) +{ + if (!m_useRtAudio) { + return; + } +#ifdef RT_AUDIO + m_outputDevice = device; emit outputDeviceChanged(m_outputDevice, false); emit outputDeviceSelected(m_outputDevice); #endif } +#ifdef RT_AUDIO +int VirtualStudio::baseOutputChannel() +{ + if (m_useRtAudio) { + return m_baseOutputChannel; + } + return 0; +} + +void VirtualStudio::setBaseOutputChannel([[maybe_unused]] int baseChannel) +{ + if (!m_useRtAudio) { + return; + } + m_baseOutputChannel = baseChannel; + emit baseOutputChannelChanged(m_baseOutputChannel, true); +} + +int VirtualStudio::numOutputChannels() +{ + if (m_useRtAudio) { + return m_numOutputChannels; + } + return 0; +} + +void VirtualStudio::setNumOutputChannels([[maybe_unused]] int numChannels) +{ + if (!m_useRtAudio) { + return; + } + m_numOutputChannels = numChannels; + emit numOutputChannelsChanged(m_numOutputChannels, true); +} +#endif + int VirtualStudio::previousInput() { #ifdef RT_AUDIO if (m_useRtAudio) { - QStringList filteredInputDeviceList; - for (int i = 0; i < m_inputDeviceList.size(); i++) { - if (m_inputDeviceList.at(i) != "(default)") { - filteredInputDeviceList += m_inputDeviceList.at(i); - } - } - - int index = filteredInputDeviceList.indexOf(m_previousInput); + int index = m_inputDeviceList.indexOf(m_previousInput); return index >= 0 ? index : 0; } #endif @@ -417,14 +547,7 @@ void VirtualStudio::setPreviousInput([[maybe_unused]] int device) return; } #ifdef RT_AUDIO - QStringList filteredInputDeviceList; - for (int i = 0; i < m_inputDeviceList.size(); i++) { - if (m_inputDeviceList.at(i) != "(default)") { - filteredInputDeviceList += m_inputDeviceList.at(i); - } - } - - m_previousInput = filteredInputDeviceList.at(device); + m_previousInput = m_inputDeviceList.at(device); emit previousInputChanged(); #endif } @@ -433,14 +556,7 @@ int VirtualStudio::previousOutput() { #ifdef RT_AUDIO if (m_useRtAudio) { - QStringList filteredOutputDeviceList; - for (int i = 0; i < m_outputDeviceList.size(); i++) { - if (m_outputDeviceList.at(i) != "(default)") { - filteredOutputDeviceList += m_outputDeviceList.at(i); - } - } - - int index = filteredOutputDeviceList.indexOf(m_previousOutput); + int index = m_outputDeviceList.indexOf(m_previousOutput); return index >= 0 ? index : 0; } #endif @@ -453,14 +569,7 @@ void VirtualStudio::setPreviousOutput([[maybe_unused]] int device) return; } #ifdef RT_AUDIO - QStringList filteredOutputDeviceList; - for (int i = 0; i < m_outputDeviceList.size(); i++) { - if (m_outputDeviceList.at(i) != "(default)") { - filteredOutputDeviceList += m_outputDeviceList.at(i); - } - } - - m_previousOutput = filteredOutputDeviceList.at(device); + m_previousOutput = m_outputDeviceList.at(device); emit previousOutputChanged(); #endif } @@ -879,6 +988,7 @@ void VirtualStudio::toStandard() if (!m_standardWindow.isNull()) { m_view.hide(); m_standardWindow->show(); + m_vsModeActive = false; } QSettings settings; settings.setValue(QStringLiteral("UiMode"), QJackTrip::STANDARD); @@ -906,7 +1016,6 @@ void VirtualStudio::toVirtualStudio() (*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code); } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) { parameters->insert(QStringLiteral("audience"), AUTH_AUDIENCE); - parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); } if (!parameters->contains("client_id")) { parameters->insert("client_id", AUTH_CLIENT_ID); @@ -930,6 +1039,20 @@ void VirtualStudio::logout() m_device->removeApp(); } + QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout"); + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID); + if (m_testMode) { + query.addQueryItem(QStringLiteral("returnTo"), + QStringLiteral("https://test.jacktrip.org/")); + } else { + query.addQueryItem(QStringLiteral("returnTo"), + QStringLiteral("https://app.jacktrip.org/")); + } + + logoutURL.setQuery(query); + launchBrowser(logoutURL); + m_authenticator->setToken(QLatin1String("")); m_authenticator->setRefreshToken(QLatin1String("")); @@ -961,30 +1084,271 @@ void VirtualStudio::refreshStudios(int index, bool signalRefresh) void VirtualStudio::refreshDevices() { #ifdef RT_AUDIO - RtAudioInterface::getDeviceList(&m_inputDeviceList, &m_inputDeviceCategories, true); + if (!m_vsAudioInterface.isNull()) { + m_vsAudioInterface->closeAudio(); + setAudioReady(false); + } + + RtAudioInterface::getDeviceList(&m_inputDeviceList, &m_inputDeviceCategories, + &m_inputDeviceChannels, true); RtAudioInterface::getDeviceList(&m_outputDeviceList, &m_outputDeviceCategories, - false); + &m_outputDeviceChannels, false); - QVariant inputComboModel = - formatDeviceList(m_inputDeviceList, m_inputDeviceCategories); - QVariant outputComboModel = - formatDeviceList(m_outputDeviceList, m_outputDeviceCategories); + QVariant inputComboModel = formatDeviceList( + m_inputDeviceList, m_inputDeviceCategories, m_inputDeviceChannels); + QVariant outputComboModel = formatDeviceList( + m_outputDeviceList, m_outputDeviceCategories, m_outputDeviceChannels); m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputComboModel"), inputComboModel); m_view.engine()->rootContext()->setContextProperty(QStringLiteral("outputComboModel"), outputComboModel); + validateDevicesState(); + if (!m_vsAudioInterface.isNull()) { + restartAudio(); + } +#endif +} - // Make sure we keep our current settings if the device still exists - if (!m_inputDeviceList.contains(m_inputDevice)) { - m_inputDevice = QStringLiteral("(default)"); +void VirtualStudio::validateDevicesState() +{ + validateInputDevicesState(); + validateOutputDevicesState(); +} + +void VirtualStudio::validateInputDevicesState() +{ + if (!m_useRtAudio) { + return; } - if (!m_outputDeviceList.contains(m_outputDevice)) { - m_outputDevice = QStringLiteral("(default)"); +#ifdef RT_AUDIO + if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { + return; } - emit inputDeviceChanged(m_inputDevice, false); - emit outputDeviceChanged(m_outputDevice, false); -#endif + // Given input device list, check that the currently set device + // actually exists + if (m_inputDevice == QStringLiteral("") + || m_inputDeviceList.indexOf(m_inputDevice) == -1) { + m_inputDevice = m_inputDeviceList[0]; + emit inputDeviceChanged(m_inputDevice, false); + } + + // Given the currently selected input device, reset the available input channel + // options + int indexOfInput = m_inputDeviceList.indexOf(m_inputDevice); + if (indexOfInput == -1) { + std::cerr << "Invalid state. Input device index should never be -1" << std::endl; + return; + } + + int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput); + if (numDevicesChannelsAvailable < 1) { + std::cerr << "Invalid state. Number of channels should never be less than 1" + << std::endl; + return; + } else if (numDevicesChannelsAvailable == 1) { + // Set the input mix mode to just have "Mono" as the option + QJsonObject inputMixModeComboElement = QJsonObject(); + inputMixModeComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputMixModeComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(inputMixModeComboElement))))); + + // Set the input channels combo to only have channel 1 as an option + QJsonObject inputChannelsComboElement = QJsonObject(); + inputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1")); + inputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + inputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(1).toInt()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputChannelsComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(inputChannelsComboElement))))); + + // Set the only allowed options for these variables automatically + m_baseInputChannel = 0; + m_numInputChannels = 1; + m_inputMixMode = static_cast(AudioInterface::MONO); + + emit baseInputChannelChanged(m_baseInputChannel, false); + emit numInputChannelsChanged(m_numInputChannels, false); + emit inputMixModeChanged(m_inputMixMode, false); + } else { + // set the input channels selector to have the options based on the currently + // selected device + QVariantList items = QVariantList(); + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + QJsonObject element = QJsonObject(); + element.insert(QString::fromStdString("label"), QVariant(i + 1).toString()); + element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt()); + items.push_back(QVariant(QJsonValue(element))); + } + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + if (i % 2 == 0) { + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), + QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), + QVariant(2).toInt()); + items.push_back(QVariant(QJsonValue(element))); + } + } + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputChannelsComboModel"), QVariant(items)); + + // if the current m_baseInputChannel or m_numInputChannels is invalid based on + // this device's option, use the first two channels by default + if (m_baseInputChannel + m_numInputChannels > numDevicesChannelsAvailable) { + // we're in the case where numDevicesChannelsAvailable >= 2, so always have + // the ability to use the first 2 channels + m_baseInputChannel = 0; + m_numInputChannels = 2; + emit baseInputChannelChanged(m_baseInputChannel, false); + emit numInputChannelsChanged(m_numInputChannels, false); + } + if (m_numInputChannels != 1) { + // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if + // we're using 2 channels + QJsonObject inputMixModeComboElement1 = QJsonObject(); + inputMixModeComboElement1.insert(QString::fromStdString("label"), + QString::fromStdString("Stereo")); + inputMixModeComboElement1.insert(QString::fromStdString("value"), + static_cast(AudioInterface::STEREO)); + QJsonObject inputMixModeComboElement2 = QJsonObject(); + inputMixModeComboElement2.insert(QString::fromStdString("label"), + QString::fromStdString("Mix to Mono")); + inputMixModeComboElement2.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MIXTOMONO)); + + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputMixModeComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(inputMixModeComboElement1)) + << QVariant(QJsonValue(inputMixModeComboElement2))))); + + // if m_inputMixMode is an invalid value, set it to "stereo" by default + // given that we are using 2 channels + if (m_inputMixMode != static_cast(AudioInterface::STEREO) + && m_inputMixMode != static_cast(AudioInterface::MIXTOMONO)) { + m_inputMixMode = static_cast(AudioInterface::STEREO); + emit inputMixModeChanged(m_inputMixMode, false); + } + } else { + // Set the input mix mode to just have "Mono" as the option if we're using 1 + // channel + QJsonObject inputMixModeComboElement = QJsonObject(); + inputMixModeComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("inputMixModeComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(inputMixModeComboElement))))); + + // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO + if (m_inputMixMode != static_cast(AudioInterface::MONO)) { + m_inputMixMode = static_cast(AudioInterface::MONO); + emit inputMixModeChanged(m_inputMixMode, false); + } + } + } +#endif // RT_AUDIO +} + +void VirtualStudio::validateOutputDevicesState() +{ + if (!m_useRtAudio) { + return; + } +#ifdef RT_AUDIO + if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { + return; + } + + // Given output device list, check that the currently set device + // actually exists + if (m_outputDevice == QStringLiteral("") + || m_outputDeviceList.indexOf(m_outputDevice) == -1) { + m_outputDevice = m_outputDeviceList[0]; + emit outputDeviceChanged(m_outputDevice, false); + } + + // Given the currently selected output device, reset the available output channel + // options + int indexOfOutput = m_outputDeviceList.indexOf(m_outputDevice); + if (indexOfOutput == -1) { + std::cerr << "Invalid state. Output device index should never be -1" << std::endl; + return; + } + + int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput); + if (numDevicesChannelsAvailable < 1) { + std::cerr << "Invalid state. Number of channels should never be less than 1" + << std::endl; + return; + } else if (numDevicesChannelsAvailable == 1) { + // Set the output channels combo to only have channel 1 as an option + QJsonObject outputChannelsComboElement = QJsonObject(); + outputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1")); + outputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + outputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(1).toInt()); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputChannelsComboModel"), + QVariant::fromValue(QVariant( + QVariantList() << QVariant(QJsonValue(outputChannelsComboElement))))); + + // Set the only allowed options for these variables automatically + m_baseOutputChannel = 0; + m_numOutputChannels = 1; + + emit baseOutputChannelChanged(m_baseOutputChannel, false); + emit numOutputChannelsChanged(m_numOutputChannels, false); + } else { + // set the output channels selector to have the options based on the currently + // selected device + QVariantList items = QVariantList(); + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + if (i % 2 == 0) { + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), + QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), + QVariant(2).toInt()); + items.push_back(QVariant(QJsonValue(element))); + } + } + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputChannelsComboModel"), QVariant(items)); + + // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on + // this device's option, use the first two channels by default + if (m_baseOutputChannel + m_numOutputChannels > numDevicesChannelsAvailable) { + // we're in the case where numDevicesChannelsAvailable >= 2, so always have + // the ability to use the first 2 channels + m_baseOutputChannel = 0; + m_numOutputChannels = 2; + emit baseOutputChannelChanged(m_baseOutputChannel, false); + emit numOutputChannelsChanged(m_numOutputChannels, false); + } + } +#endif // RT_AUDIO } void VirtualStudio::playOutputAudio() @@ -1007,6 +1371,9 @@ void VirtualStudio::revertSettings() emit inputDeviceChanged(m_inputDevice, false); emit outputDeviceChanged(m_outputDevice, false); + emit baseInputChannelChanged(m_baseInputChannel, false); + emit numInputChannelsChanged(m_numInputChannels, false); + emit inputMixModeChanged(m_inputMixMode, false); emit bufferSizeChanged(); if (m_useRtAudio != m_previousUseRtAudio) { emit audioBackendChanged(m_useRtAudio, false); @@ -1032,6 +1399,11 @@ void VirtualStudio::applySettings() settings.setValue(QStringLiteral("BufferSize"), m_bufferSize); settings.setValue(QStringLiteral("InputDevice"), m_inputDevice); settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice); + settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel); + settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels); + settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode); + settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel); + settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels); settings.endGroup(); m_previousUseRtAudio = m_useRtAudio; @@ -1043,6 +1415,9 @@ void VirtualStudio::applySettings() emit previousOutputChanged(); emit inputDeviceChanged(m_inputDevice, false); emit outputDeviceChanged(m_outputDevice, false); + emit baseInputChannelChanged(m_baseInputChannel, false); + emit numInputChannelsChanged(m_numInputChannels, false); + emit inputMixModeChanged(m_inputMixMode, false); #endif // attempt to join studio if requested @@ -1081,7 +1456,7 @@ void VirtualStudio::connectToStudio(int studioIndex) m_studioSocket->openSocket(); // Check if we have an address for our server - if (studioInfo->status() != "Ready" && studioInfo->isManageable() == true) { + if (studioInfo->status() != "Ready" && studioInfo->isAdmin() == true) { m_connectionState = QStringLiteral("Waiting..."); emit connectionStateChanged(); } else { @@ -1109,19 +1484,28 @@ void VirtualStudio::completeConnection() int buffer_size = 0; #ifdef RT_AUDIO if (m_useRtAudio) { - input = m_inputDevice.toStdString(); - if (m_inputDevice == QLatin1String("(default)")) { - input = ""; - } - output = m_outputDevice.toStdString(); - if (m_outputDevice == QLatin1String("(default)")) { - output = ""; - } + input = m_inputDevice.toStdString(); + output = m_outputDevice.toStdString(); buffer_size = m_bufferSize; } + int inputMixMode = m_inputMixMode; +#else + int inputMixMode = -1; #endif JackTrip* jackTrip = m_device->initJackTrip( - m_useRtAudio, input, output, buffer_size, m_bufferStrategy, studioInfo); + m_useRtAudio, input, output, +#ifdef RT_AUDIO + m_baseInputChannel, m_numInputChannels, m_baseOutputChannel, + m_numOutputChannels, +#else + 0, 2, 0, 2, // default to 2 channels for input and 2 channels for output + // starting at channel 0 +#endif + inputMixMode, buffer_size, m_bufferStrategy, studioInfo); + if (jackTrip == 0) { + processError("Could not bind port"); + return; + } QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this, &VirtualStudio::processFinished, Qt::QueuedConnection); @@ -1150,13 +1534,13 @@ void VirtualStudio::completeConnection() &Volume::muteUpdated); // Setup output meter - Meter* m_outputMeter = new Meter(jackTrip->getNumOutputChannels()); + m_outputMeter = new Meter(jackTrip->getNumOutputChannels()); jackTrip->appendProcessPluginFromNetwork(m_outputMeter); connect(m_outputMeter, &Meter::onComputedVolumeMeasurements, this, &VirtualStudio::updatedOutputVuMeasurements); // Setup input meter - Meter* m_inputMeter = new Meter(jackTrip->getNumInputChannels()); + m_inputMeter = new Meter(jackTrip->getNumInputChannels()); jackTrip->appendProcessPluginToNetwork(m_inputMeter); connect(m_inputMeter, &Meter::onComputedVolumeMeasurements, this, &VirtualStudio::updatedInputVuMeasurements); @@ -1237,6 +1621,12 @@ void VirtualStudio::disconnect() m_connectionState = QStringLiteral("Disconnected"); emit connectionStateChanged(); + + // cleanup + m_inputMeter = nullptr; + m_outputMeter = nullptr; + m_inputVolumePlugin = nullptr; + m_outputVolumePlugin = nullptr; } void VirtualStudio::manageStudio(int studioIndex, bool start) @@ -1349,6 +1739,7 @@ void VirtualStudio::slotAuthSucceded() settings.beginGroup(QStringLiteral("VirtualStudio")); settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken); settings.endGroup(); + m_vsModeActive = true; m_device = new VsDevice(m_authenticator.data(), m_testMode); m_device->registerApp(); @@ -1564,7 +1955,8 @@ void VirtualStudio::updatedDevicesWarningHelpUrl(const QString& url) return; } -void VirtualStudio::updatedInputVuMeasurements(const QVector& valuesInDecibels) +void VirtualStudio::updatedInputVuMeasurements(const float* valuesInDecibels, + int numChannels) { QJsonArray uiValues; bool detectedClip = false; @@ -1573,7 +1965,7 @@ void VirtualStudio::updatedInputVuMeasurements(const QVector& valuesInDec for (int i = 0; i < 2; i++) { // Determine decibel reading float dB = m_meterMin; - if (i < valuesInDecibels.size()) { + if (i < numChannels) { dB = std::max(m_meterMin, valuesInDecibels[i]); } @@ -1592,11 +1984,23 @@ void VirtualStudio::updatedInputVuMeasurements(const QVector& valuesInDec } } +#ifdef RT_AUDIO + // For certain specific cases, copy the first channel's value into the second + // channel's value + if ((m_inputMixMode == static_cast(AudioInterface::MONO) + && m_numInputChannels == 1) + || (m_inputMixMode == static_cast(AudioInterface::MIXTOMONO) + && m_numInputChannels == 2)) { + uiValues[1] = uiValues[0]; + } +#endif + m_view.engine()->rootContext()->setContextProperty(QStringLiteral("inputMeterModel"), QVariant::fromValue(uiValues)); } -void VirtualStudio::updatedOutputVuMeasurements(const QVector& valuesInDecibels) +void VirtualStudio::updatedOutputVuMeasurements(const float* valuesInDecibels, + int numChannels) { QJsonArray uiValues; bool detectedClip = false; @@ -1605,7 +2009,7 @@ void VirtualStudio::updatedOutputVuMeasurements(const QVector& valuesInDe for (int i = 0; i < 2; i++) { // Determine decibel reading float dB = m_meterMin; - if (i < valuesInDecibels.size()) { + if (i < numChannels) { dB = std::max(m_meterMin, valuesInDecibels[i]); } @@ -1623,7 +2027,11 @@ void VirtualStudio::updatedOutputVuMeasurements(const QVector& valuesInDe detectedClip = true; } } - +#ifdef RT_AUDIO + if (m_numOutputChannels == 1) { + uiValues[1] = uiValues[0]; + } +#endif m_view.engine()->rootContext()->setContextProperty(QStringLiteral("outputMeterModel"), QVariant::fromValue(uiValues)); } @@ -1653,7 +2061,6 @@ void VirtualStudio::setupAuthenticator() } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) { parameters->insert(QStringLiteral("audience"), QStringLiteral("https://api.jacktrip.org")); - parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); } }); @@ -1668,6 +2075,9 @@ void VirtualStudio::setupAuthenticator() "Studio Login Successful\n" "

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

\n" + "

Alternatively, " + " click " + "here  to create your first studio.

\n" "\n")); m_authenticator->setReplyHandler(replyHandler); connect(m_authenticator.data(), &QOAuth2AuthorizationCodeFlow::granted, this, @@ -1744,20 +2154,20 @@ void VirtualStudio::getServerList(bool firstLoad, bool signalRefresh, int index) if (servers.at(i)[QStringLiteral("type")].toString().contains( QStringLiteral("JackTrip"))) { VsServerInfo* serverInfo = new VsServerInfo(this); - serverInfo->setIsManageable( + serverInfo->setIsAdmin( servers.at(i)[QStringLiteral("admin")].toBool()); QString status = servers.at(i)[QStringLiteral("status")].toString(); bool activeStudio = status == QLatin1String("Ready"); bool hostedStudio = servers.at(i)[QStringLiteral("managed")].toBool(); // Only iterate through servers that we want to show if (!m_showSelfHosted && !hostedStudio) { - if (activeStudio || (serverInfo->isManageable())) { + if (activeStudio || (serverInfo->isAdmin())) { skippedStudios++; } continue; } if (!m_showInactive && !activeStudio) { - if (serverInfo->isManageable()) { + if (serverInfo->isAdmin()) { skippedStudios++; } continue; @@ -1767,6 +2177,8 @@ void VirtualStudio::getServerList(bool firstLoad, bool signalRefresh, int index) servers.at(i)[QStringLiteral("name")].toString()); serverInfo->setHost( servers.at(i)[QStringLiteral("serverHost")].toString()); + serverInfo->setIsManaged( + servers.at(i)[QStringLiteral("managed")].toBool()); serverInfo->setStatus( servers.at(i)[QStringLiteral("status")].toString()); serverInfo->setPort( @@ -1794,8 +2206,6 @@ void VirtualStudio::getServerList(bool firstLoad, bool signalRefresh, int index) servers.at(i)[QStringLiteral("enabled")].toBool()); serverInfo->setIsOwner( servers.at(i)[QStringLiteral("owner")].toBool()); - serverInfo->setIsAdmin( - servers.at(i)[QStringLiteral("admin")].toBool()); if (servers.at(i)[QStringLiteral("owner")].toBool()) { yourServers.append(serverInfo); serverInfo->setSection(VsServerInfo::YOUR_STUDIOS); @@ -2001,9 +2411,15 @@ void VirtualStudio::startAudio() QStringLiteral("audioInterface"), m_vsAudioInterface.data()); } #ifdef RT_AUDIO + validateDevicesState(); m_vsAudioInterface->setInputDevice(m_inputDevice, false); m_vsAudioInterface->setOutputDevice(m_outputDevice, false); - m_vsAudioInterface->setAudioInterfaceMode(m_useRtAudio); + m_vsAudioInterface->setAudioInterfaceMode(m_useRtAudio, false); + m_vsAudioInterface->setBaseInputChannel(m_baseInputChannel, false); + m_vsAudioInterface->setNumInputChannels(m_numInputChannels, false); + m_vsAudioInterface->setInputMixMode(m_inputMixMode, false); + m_vsAudioInterface->setBaseOutputChannel(m_baseOutputChannel, false); + m_vsAudioInterface->setNumOutputChannels(m_numOutputChannels, false); #endif connect(m_vsAudioInterface.data(), &VsAudioInterface::devicesErrorMsgChanged, this, &VirtualStudio::updatedDevicesErrorMsg); @@ -2023,12 +2439,26 @@ void VirtualStudio::startAudio() &VsAudioInterface::setOutputDevice); connect(this, &VirtualStudio::outputDeviceSelected, m_vsAudioInterface.data(), &VsAudioInterface::setOutputDevice); +#ifdef RT_AUDIO + connect(this, &VirtualStudio::baseInputChannelChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setBaseInputChannel); + connect(this, &VirtualStudio::numInputChannelsChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setNumInputChannels); + connect(this, &VirtualStudio::inputMixModeChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setInputMixMode); + connect(this, &VirtualStudio::baseOutputChannelChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setBaseOutputChannel); + connect(this, &VirtualStudio::numOutputChannelsChanged, m_vsAudioInterface.data(), + &VsAudioInterface::setNumOutputChannels); +#endif connect(this, &VirtualStudio::audioBackendChanged, m_vsAudioInterface.data(), &VsAudioInterface::setAudioInterfaceMode); connect(this, &VirtualStudio::triggerPlayOutputAudio, m_vsAudioInterface.data(), &VsAudioInterface::triggerPlayback); - connect(m_vsAudioInterface.data(), &VsAudioInterface::newVolumeMeterMeasurements, - this, &VirtualStudio::updatedInputVuMeasurements); + connect(m_vsAudioInterface.data(), &VsAudioInterface::newInputMeterMeasurements, this, + &VirtualStudio::updatedInputVuMeasurements); + connect(m_vsAudioInterface.data(), &VsAudioInterface::newOutputMeterMeasurements, + this, &VirtualStudio::updatedOutputVuMeasurements); connect(m_vsAudioInterface.data(), &VsAudioInterface::errorToProcess, this, &VirtualStudio::processError); @@ -2041,6 +2471,10 @@ void VirtualStudio::startAudio() QStringLiteral("inputMeterModel"), QVariant::fromValue(QVector(m_vsAudioInterface->getNumInputChannels()))); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputMeterModel"), + QVariant::fromValue(QVector(m_vsAudioInterface->getNumOutputChannels()))); + m_vsAudioInterface->startProcess(); } @@ -2053,6 +2487,11 @@ void VirtualStudio::restartAudio() #endif // Start VsAudioInterface again if (!m_vsAudioInterface.isNull()) { +#ifdef RT_AUDIO + validateDevicesState(); + m_vsAudioInterface->setInputDevice(m_inputDevice, false); + m_vsAudioInterface->setOutputDevice(m_outputDevice, false); +#endif m_vsAudioInterface->setupAudio(); m_vsAudioInterface->setupPlugins(); @@ -2064,6 +2503,11 @@ void VirtualStudio::restartAudio() QVariant::fromValue( QVector(m_vsAudioInterface->getNumInputChannels()))); + m_view.engine()->rootContext()->setContextProperty( + QStringLiteral("outputMeterModel"), + QVariant::fromValue( + QVector(m_vsAudioInterface->getNumOutputChannels()))); + m_vsAudioInterface->startProcess(); } else { startAudio(); @@ -2131,19 +2575,10 @@ bool VirtualStudio::readyToJoin() #ifdef RT_AUDIO QVariant VirtualStudio::formatDeviceList(const QStringList& devices, - const QStringList& categories) + const QStringList& categories, + const QList& channels) { - QStringList filteredDevices; - QStringList filteredCategories; - - for (int i = 0; i < devices.size(); i++) { - if (!devices[i].contains("(default)")) { - filteredDevices += devices[i]; - filteredCategories += categories[i]; - } - } - - QStringList uniqueCategories = QStringList(filteredCategories); + QStringList uniqueCategories = QStringList(categories); uniqueCategories.removeDuplicates(); bool containsCategories = true; @@ -2165,12 +2600,13 @@ QVariant VirtualStudio::formatDeviceList(const QStringList& devices, items.push_back(QVariant(QJsonValue(header))); } - for (int j = 0; j < filteredDevices.size(); j++) { - if (filteredCategories.at(j).toStdString() == category.toStdString()) { + for (int j = 0; j < devices.size(); j++) { + if (categories.at(j).toStdString() == category.toStdString()) { QJsonObject element = QJsonObject(); - element.insert(QString::fromStdString("text"), filteredDevices.at(j)); + element.insert(QString::fromStdString("text"), devices.at(j)); element.insert(QString::fromStdString("type"), QString::fromStdString("element")); + element.insert(QString::fromStdString("channels"), channels.at(j)); items.push_back(QVariant(QJsonValue(element))); } } @@ -2188,6 +2624,8 @@ VirtualStudio::~VirtualStudio() delete m_inputMeter; delete m_outputMeter; + delete m_outputVolumePlugin; + delete m_inputVolumePlugin; delete m_inputTestMeter; delete m_studioSocket; diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h index f6d91f4..eb097d5 100644 --- a/src/gui/virtualstudio.h +++ b/src/gui/virtualstudio.h @@ -31,7 +31,7 @@ /** * \file virtualstudio.h - * \author Aaron Wyatt + * \author Matt Horton, based on code by Aaron Wyatt * \date March 2022 */ @@ -78,15 +78,26 @@ class VirtualStudio : public QObject 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 + Q_PROPERTY(QString inputDevice READ inputDevice WRITE setInputDevice NOTIFY + inputDeviceChanged) + Q_PROPERTY(QString outputDevice READ outputDevice WRITE setOutputDevice NOTIFY outputDeviceChanged) Q_PROPERTY(int previousInput READ previousInput WRITE setPreviousInput NOTIFY previousInputChanged) Q_PROPERTY(int previousOutput READ previousOutput WRITE setPreviousOutput NOTIFY previousOutputChanged) - +#ifdef RT_AUDIO + Q_PROPERTY(int baseInputChannel READ baseInputChannel WRITE setBaseInputChannel NOTIFY + baseInputChannelChanged) + Q_PROPERTY(int numInputChannels READ numInputChannels WRITE setNumInputChannels NOTIFY + numInputChannelsChanged) + Q_PROPERTY(int inputMixMode READ inputMixMode WRITE setInputMixMode NOTIFY + inputMixModeChanged) + Q_PROPERTY(int baseOutputChannel READ baseOutputChannel WRITE setBaseOutputChannel + NOTIFY baseOutputChannelChanged) + Q_PROPERTY(int numOutputChannels READ numOutputChannels WRITE setNumOutputChannels + NOTIFY numOutputChannelsChanged) +#endif Q_PROPERTY(QString devicesWarning READ devicesWarning NOTIFY devicesWarningChanged) Q_PROPERTY(QString devicesError READ devicesError NOTIFY devicesErrorChanged) Q_PROPERTY(QString devicesWarningHelpUrl READ devicesWarningHelpUrl NOTIFY @@ -99,6 +110,8 @@ class VirtualStudio : public QObject Q_PROPERTY(int bufferStrategy READ bufferStrategy WRITE setBufferStrategy NOTIFY bufferStrategyChanged) Q_PROPERTY(int currentStudio READ currentStudio NOTIFY currentStudioChanged) + Q_PROPERTY(QUrl studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY + studioToJoinChanged) Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged) Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged) Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY @@ -145,6 +158,7 @@ class VirtualStudio : public QObject void setStandardWindow(QSharedPointer window); void show(); void raiseToTop(); + bool vsModeActive(); bool showFirstRun(); void setShowFirstRun(bool show); @@ -154,10 +168,24 @@ class VirtualStudio : public QObject bool selectableBackend(); QString audioBackend(); void setAudioBackend(const QString& backend); - int inputDevice(); - void setInputDevice(int device); - int outputDevice(); - void setOutputDevice(int device); + QString inputDevice(); + void setInputDevice(const QString& device); +#ifdef RT_AUDIO + int baseInputChannel(); + void setBaseInputChannel(int baseChannel); + int numInputChannels(); + void setNumInputChannels(int numChannels); + void setInputMixMode(int mode); + int inputMixMode(); +#endif + QString outputDevice(); + void setOutputDevice(const QString& device); +#ifdef RT_AUDIO + int baseOutputChannel(); + void setBaseOutputChannel(int baseChannel); + int numOutputChannels(); + void setNumOutputChannels(int numChannels); +#endif int previousInput(); void setPreviousInput(int device); int previousOutput(); @@ -220,6 +248,9 @@ class VirtualStudio : public QObject void logout(); void refreshStudios(int index, bool signalRefresh = false); void refreshDevices(); + void validateDevicesState(); + void validateInputDevicesState(); + void validateOutputDevicesState(); void playOutputAudio(); void revertSettings(); void applySettings(); @@ -232,8 +263,8 @@ class VirtualStudio : public QObject void editProfile(); void showAbout(); void openLink(const QString& url); - void updatedInputVuMeasurements(const QVector& valuesInDecibels); - void updatedOutputVuMeasurements(const QVector& valuesInDecibels); + void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels); + void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels); void setInputVolume(float multiplier); void setOutputVolume(float multiplier); void setInputMuted(bool muted); @@ -255,9 +286,14 @@ class VirtualStudio : public QObject void logoSectionChanged(); void audioBackendChanged(bool useRtAudio, bool shouldRestart = true); void inputDeviceChanged(QString device, bool shouldRestart = true); + void baseInputChannelChanged(int baseChannel, bool shouldRestart = true); + void numInputChannelsChanged(int numChannels, bool shouldRestart = true); + void inputMixModeChanged(int mode, bool shouldRestart = true); void outputDeviceChanged(QString device, bool shouldRestart = true); void inputDeviceSelected(QString device, bool shouldRestart = true); void outputDeviceSelected(QString device, bool shouldRestart = true); + void baseOutputChannelChanged(int baseChannel, bool shouldRestart = true); + void numOutputChannelsChanged(int numChannels, bool shouldRestart = true); void previousInputChanged(); void previousOutputChanged(); void devicesWarningChanged(); @@ -327,11 +363,13 @@ class VirtualStudio : public QObject void stopAudio(); bool readyToJoin(); #ifdef RT_AUDIO - QVariant formatDeviceList(const QStringList& devices, const QStringList& categories); + QVariant formatDeviceList(const QStringList& devices, const QStringList& categories, + const QList& channels); #endif bool m_showFirstRun = false; bool m_checkSsl = true; + bool m_vsModeActive = false; QString m_updateChannel; QString m_refreshToken; QString m_userId; @@ -415,12 +453,22 @@ class VirtualStudio : public QObject QStringList m_outputDeviceList; QStringList m_inputDeviceCategories; QStringList m_outputDeviceCategories; + QList m_inputDeviceChannels; + QList m_outputDeviceChannels; QString m_inputDevice; QString m_outputDevice; quint16 m_bufferSize; QString m_previousInput; QString m_previousOutput; quint16 m_previousBuffer; + + int m_baseInputChannel; + int m_numInputChannels; + int m_inputMixMode; + + int m_baseOutputChannel; + int m_numOutputChannels; + bool m_previousUseRtAudio = false; inline void delay(int millisecondsWait) { diff --git a/src/gui/vs.qml b/src/gui/vs.qml index b694d56..8ca2095 100644 --- a/src/gui/vs.qml +++ b/src/gui/vs.qml @@ -125,23 +125,23 @@ Rectangle { Connections { target: virtualstudio - onAuthSucceeded: { + function onAuthSucceeded() { if (virtualstudio.showDeviceSetup) { virtualstudio.windowState = "setup"; } else { virtualstudio.windowState = "browse"; } } - onAuthFailed: { + function onAuthFailed() { loginScreen.failTextVisible = true; } - onConnected: { + function onConnected() { virtualstudio.windowState = "connected"; } - onFailed: { + function onFailed() { virtualstudio.windowState = "failed"; } - onDisconnected: { + function onDisconnected() { virtualstudio.windowState = "browse"; } } diff --git a/src/gui/vsAudioInterface.cpp b/src/gui/vsAudioInterface.cpp index 45a65da..a27a6c7 100644 --- a/src/gui/vsAudioInterface.cpp +++ b/src/gui/vsAudioInterface.cpp @@ -161,8 +161,21 @@ void VsAudioInterface::setupJackAudio() if (gVerboseFlag) std::cout << " JackTrip:setupAudio before new JackAudioInterface" << std::endl; - m_audioInterface.reset(new JackAudioInterface( - m_numAudioChansIn, m_numAudioChansOut, m_audioBitResolution)); + + QVarLengthArray inputChans; + QVarLengthArray outputChans; + inputChans.resize(m_numAudioChansIn); + outputChans.resize(m_numAudioChansOut); + + for (int i = 0; i < m_numAudioChansIn; i++) { + inputChans[i] = 1 + i; + } + for (int i = 0; i < m_numAudioChansOut; i++) { + outputChans[i] = 1 + i; + } + + m_audioInterface.reset( + new JackAudioInterface(inputChans, outputChans, m_audioBitResolution)); m_audioInterface->setClientName(QStringLiteral("JackTrip")); @@ -211,20 +224,30 @@ void VsAudioInterface::setupRtAudio() #ifdef RT_AUDIO if constexpr (isBackendAvailable() || isBackendAvailable()) { - m_audioInterface.reset(new RtAudioInterface(m_numAudioChansIn, m_numAudioChansOut, - m_audioBitResolution)); + QVarLengthArray inputChans; + QVarLengthArray outputChans; + inputChans.resize(m_numAudioChansIn); + outputChans.resize(m_numAudioChansOut); + + for (int i = 0; i < m_numAudioChansIn; i++) { + inputChans[i] = m_baseInputChannel + i; + } + for (int i = 0; i < m_numAudioChansOut; i++) { + outputChans[i] = m_baseOutputChannel + i; + } + + m_audioInterface.reset(new RtAudioInterface( + inputChans, outputChans, + static_cast(m_inputMixMode), + m_audioBitResolution)); m_audioInterface->setSampleRate(m_sampleRate); m_audioInterface->setDeviceID(m_deviceID); m_audioInterface->setInputDevice(m_inputDeviceName); m_audioInterface->setOutputDevice(m_outputDeviceName); m_audioInterface->setBufferSizeInSamples(m_audioBufferSize); + // Note: setup might change the number of channels and/or buffer size m_audioInterface->setup(true); - // Setup might have reduced number of channels - m_numAudioChansIn = m_audioInterface->getNumInputChannels(); - m_numAudioChansOut = m_audioInterface->getNumOutputChannels(); - // Setup might have changed buffer size - m_audioBufferSize = m_audioInterface->getBufferSizeInSamples(); std::string devicesWarningMsg = m_audioInterface->getDevicesWarningMsg(); std::string devicesErrorMsg = m_audioInterface->getDevicesErrorMsg(); @@ -270,9 +293,7 @@ void VsAudioInterface::closeAudio() emit errorToProcess(QString::fromUtf8(e.what())); } m_audioInterface.clear(); - m_numAudioChansIn = gDefaultNumInChannels; - m_numAudioChansOut = gDefaultNumOutChannels; - m_deviceID = gDefaultDeviceID; + m_deviceID = gDefaultDeviceID; } } @@ -286,9 +307,14 @@ void VsAudioInterface::replaceProcess() } } -void VsAudioInterface::processMeterMeasurements(QVector values) +void VsAudioInterface::processInputMeterMeasurements(float* values, int numChannels) +{ + emit newInputMeterMeasurements(values, numChannels); +} + +void VsAudioInterface::processOutputMeterMeasurements(float* values, int numChannels) { - emit newVolumeMeterMeasurements(values); + emit newOutputMeterMeasurements(values, numChannels); } void VsAudioInterface::addInputPlugin(ProcessPlugin* plugin) @@ -304,32 +330,93 @@ void VsAudioInterface::addOutputPlugin(ProcessPlugin* plugin) void VsAudioInterface::setInputDevice(QString deviceName, bool shouldRestart) { m_inputDeviceName = deviceName.toStdString(); - if (m_inputDeviceName == "(default)") { - m_inputDeviceName = ""; + if (!m_audioInterface.isNull()) { + if (m_audioActive && shouldRestart) { + emit settingsUpdated(); + } } +} +#ifdef RT_AUDIO +void VsAudioInterface::setBaseInputChannel(int baseChannel, bool shouldRestart) +{ + if (m_audioInterfaceMode != VsAudioInterface::RTAUDIO) { + return; + } + m_baseInputChannel = baseChannel; + if (!m_audioInterface.isNull()) { + if (m_audioActive && shouldRestart) { + emit settingsUpdated(); + } + } + return; +} + +void VsAudioInterface::setNumInputChannels(int numChannels, bool shouldRestart) +{ + if (m_audioInterfaceMode != VsAudioInterface::RTAUDIO) { + return; + } + m_numAudioChansIn = numChannels; if (!m_audioInterface.isNull()) { - m_audioInterface->setInputDevice(m_inputDeviceName); if (m_audioActive && shouldRestart) { emit settingsUpdated(); } } } +void VsAudioInterface::setInputMixMode(const int mode, bool shouldRestart) +{ + if (m_audioInterfaceMode != VsAudioInterface::RTAUDIO) { + return; + } + m_inputMixMode = mode; + if (!m_audioInterface.isNull()) { + if (m_audioActive && shouldRestart) { + emit settingsUpdated(); + } + } + return; +} +#endif void VsAudioInterface::setOutputDevice(QString deviceName, bool shouldRestart) { m_outputDeviceName = deviceName.toStdString(); - if (m_outputDeviceName == "(default)") { - m_outputDeviceName = ""; + if (!m_audioInterface.isNull()) { + if (m_audioActive && shouldRestart) { + emit settingsUpdated(); + } } +} +#ifdef RT_AUDIO +void VsAudioInterface::setBaseOutputChannel(int baseChannel, bool shouldRestart) +{ + if (m_audioInterfaceMode != VsAudioInterface::RTAUDIO) { + return; + } + m_baseOutputChannel = baseChannel; + if (!m_audioInterface.isNull()) { + if (m_audioActive && shouldRestart) { + emit settingsUpdated(); + } + } + return; +} + +void VsAudioInterface::setNumOutputChannels(int numChannels, bool shouldRestart) +{ + if (m_audioInterfaceMode != VsAudioInterface::RTAUDIO) { + return; + } + m_numAudioChansOut = numChannels; if (!m_audioInterface.isNull()) { - m_audioInterface->setOutputDevice(m_outputDeviceName); if (m_audioActive && shouldRestart) { emit settingsUpdated(); } } } +#endif void VsAudioInterface::setAudioInterfaceMode(bool useRtAudio, bool shouldRestart) { @@ -361,6 +448,7 @@ void VsAudioInterface::setupPlugins() { // Create plugins m_inputMeter = new Meter(getNumInputChannels()); + m_outputMeter = new Meter(getNumOutputChannels()); m_inputVolumePlugin = new Volume(getNumInputChannels()); m_outputVolumePlugin = new Volume(getNumOutputChannels()); m_outputTonePlugin = new Tone(getNumOutputChannels()); @@ -370,10 +458,13 @@ void VsAudioInterface::setupPlugins() addInputPlugin(m_inputVolumePlugin); addOutputPlugin(m_outputVolumePlugin); addInputPlugin(m_inputMeter); + addOutputPlugin(m_outputMeter); // Connect plugins for communication with UI connect(m_inputMeter, &Meter::onComputedVolumeMeasurements, this, - &VsAudioInterface::processMeterMeasurements); + &VsAudioInterface::processInputMeterMeasurements); + connect(m_outputMeter, &Meter::onComputedVolumeMeasurements, this, + &VsAudioInterface::processOutputMeterMeasurements); connect(this, &VsAudioInterface::updatedInputVolume, m_inputVolumePlugin, &Volume::volumeUpdated); connect(this, &VsAudioInterface::updatedOutputVolume, m_outputVolumePlugin, diff --git a/src/gui/vsAudioInterface.h b/src/gui/vsAudioInterface.h index 9c28c9c..6a6d94a 100644 --- a/src/gui/vsAudioInterface.h +++ b/src/gui/vsAudioInterface.h @@ -93,7 +93,16 @@ class VsAudioInterface : public QObject public slots: void setInputDevice(QString deviceName, bool shouldRestart = true); +#ifdef RT_AUDIO + void setBaseInputChannel(int baseChannel, bool shouldRestart = true); + void setNumInputChannels(int numChannels, bool shouldRestart = true); + void setInputMixMode(const int mode, bool shouldRestart = true); +#endif void setOutputDevice(QString deviceName, bool shouldRestart = true); +#ifdef RT_AUDIO + void setBaseOutputChannel(int baseChannel, bool shouldRestart = true); + void setNumOutputChannels(int numChannels, bool shouldRestart = true); +#endif void setAudioInterfaceMode(bool useRtAudio, bool shouldRestart = true); void setInputVolume(float multiplier); void setOutputVolume(float multiplier); @@ -108,7 +117,8 @@ class VsAudioInterface : public QObject void triggerPlayback(); void settingsUpdated(); void modeUpdated(); - void newVolumeMeterMeasurements(QVector values); + void newInputMeterMeasurements(float* values, int numChannels); + void newOutputMeterMeasurements(float* values, int numChannels); void errorToProcess(const QString& errorMessage); void devicesErrorMsgChanged(const QString& msg); void devicesWarningMsgChanged(const QString& msg); @@ -118,7 +128,8 @@ class VsAudioInterface : public QObject private slots: // void refreshAudioStream(); void replaceProcess(); - void processMeterMeasurements(QVector values); + void processInputMeterMeasurements(float* values, int numChannels); + void processOutputMeterMeasurements(float* values, int numChannels); private: void setupJackAudio(); @@ -131,6 +142,10 @@ class VsAudioInterface : public QObject bool m_audioActive = false; bool m_hasBeenActive = false; + int m_baseInputChannel = 0; + int m_baseOutputChannel = 0; + int m_inputMixMode = 0; + // Needed in constructor int m_numAudioChansIn; ///< Number of Audio Input Channels int m_numAudioChansOut; ///< Number of Audio Output Channels @@ -142,6 +157,7 @@ class VsAudioInterface : public QObject std::string m_inputDeviceName, m_outputDeviceName; ///< RTAudio device names uint32_t m_audioBufferSize; ///< Audio buffer size to process on each callback VsAudioInterface::audiointerfaceModeT m_audioInterfaceMode; + Meter* m_outputMeter; Meter* m_inputMeter; Volume* m_inputVolumePlugin; Volume* m_outputVolumePlugin; diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp index 4f06599..08c79f2 100644 --- a/src/gui/vsDevice.cpp +++ b/src/gui/vsDevice.cpp @@ -341,18 +341,22 @@ void VsDevice::sendLevels() } // initJackTrip spawns a new jacktrip process with the desired settings -JackTrip* VsDevice::initJackTrip([[maybe_unused]] bool useRtAudio, - [[maybe_unused]] std::string input, - [[maybe_unused]] std::string output, - [[maybe_unused]] int bufferSize, - [[maybe_unused]] int bufferStrategy, - VsServerInfo* studioInfo) +JackTrip* VsDevice::initJackTrip( + [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input, + [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel, + [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel, + [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode, + [[maybe_unused]] int bufferSize, [[maybe_unused]] int bufferStrategy, + VsServerInfo* studioInfo) { - m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2, + m_jackTrip.reset( + new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel, + numChannelsIn, baseOutputChannel, numChannelsOut, + static_cast(inputMixMode), #ifdef WAIR // wair - 0, + 0, #endif // endwhere - 4, 1)); + 4, 1)); m_jackTrip->setConnectDefaultAudioPorts(true); #ifdef RT_AUDIO if (useRtAudio) { @@ -363,6 +367,11 @@ JackTrip* VsDevice::initJackTrip([[maybe_unused]] bool useRtAudio, m_jackTrip->setOutputDevice(output); } #endif + int bindPort = selectBindPort(); + if (bindPort == 0) { + return 0; + } + m_jackTrip->setBindPorts(bindPort); m_jackTrip->setRemoteClientName(m_appID); // increment m_bufferStrategy by 1 for array-index mapping m_jackTrip->setBufferStrategy(bufferStrategy + 1); @@ -662,3 +671,21 @@ QString VsDevice::randomString(int stringLength) return str; } + +// selectBindPort finds the next open bind port to use for jacktrip +int VsDevice::selectBindPort() +{ + int candidate = gDefaultPort; + if (m_jackTrip.isNull()) { + return candidate; + } + int attempt = 0; + while (attempt <= 5000) { + candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1); + attempt++; + if (!m_jackTrip->checkIfPortIsBinded(candidate)) { + return candidate; + } + } + return 0; +} diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h index 373c753..b13a733 100644 --- a/src/gui/vsDevice.h +++ b/src/gui/vsDevice.h @@ -67,7 +67,9 @@ class VsDevice : public QObject void sendHeartbeat(); void setServerId(QString studioID); JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output, - int bufferSize, int bufferStrategy, VsServerInfo* studioInfo); + int baseInputChannel, int numChannelsIn, int baseOutputChannel, + int numChannelsOut, int inputMixMode, int bufferSize, + int bufferStrategy, VsServerInfo* studioInfo); void startJackTrip(); void stopJackTrip(); void reconcileAgentConfig(QJsonDocument newState); @@ -96,6 +98,7 @@ class VsDevice : public QObject private: void registerJTAsDevice(); bool enabled(); + int selectBindPort(); QString randomString(int stringLength); VsPinger* m_pinger = NULL; diff --git a/src/gui/vsInit.cpp b/src/gui/vsInit.cpp new file mode 100644 index 0000000..b74f92b --- /dev/null +++ b/src/gui/vsInit.cpp @@ -0,0 +1,170 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsInit.cpp + * \author Aaron Wyatt, based on code by Matt Horton + * \date February 2023 + */ + +#include "vsInit.h" + +#include +#include +#include + +QString VsInit::parseDeeplink(QCoreApplication* app) +{ + // Parse command line for deep link + QCommandLineParser parser; + QCommandLineOption deeplinkOption(QStringList() << QStringLiteral("deeplink")); + deeplinkOption.setValueName(QStringLiteral("deeplink")); + parser.addOption(deeplinkOption); + parser.parse(app->arguments()); + if (parser.isSet(deeplinkOption)) { + return parser.value(deeplinkOption); + } else { + return QLatin1String(""); + } +} + +void VsInit::checkForInstance(QString& deeplink) +{ + m_deeplink = deeplink; + // Create socket + m_instanceCheckSocket.reset(new QLocalSocket(this)); + QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this, + &VsInit::connectionReceived, Qt::QueuedConnection); + // Create instanceServer to prevent new instances from being created + void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError); +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + errorFunc = &QLocalSocket::error; +#else + errorFunc = &QLocalSocket::errorOccurred; +#endif + QObject::connect(m_instanceCheckSocket.data(), errorFunc, this, + &VsInit::connectionFailed); + // Check for existing instance + m_instanceCheckSocket->connectToServer("jacktripExists"); +} + +#ifdef _WIN32 +void VsInit::setUrlScheme() +{ + // Set url scheme in registry + QString path = QDir::toNativeSeparators(qApp->applicationFilePath()); + + QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat); + set.beginGroup("jacktrip"); + set.setValue("Default", "URL:JackTrip Protocol"); + set.setValue("DefaultIcon/Default", path); + set.setValue("URL Protocol", ""); + set.setValue("shell/open/command/Default", + QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\""); + set.endGroup(); +} +#endif + +void VsInit::connectionReceived() +{ + // pass deeplink to existing instance before quitting + if (!m_deeplink.isEmpty()) { + QByteArray baDeeplink = m_deeplink.toLocal8Bit(); + qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink); + m_instanceCheckSocket->flush(); + m_instanceCheckSocket->disconnectFromServer(); // remove next + + if (writeBytes < 0) { + qDebug() << "sending deeplink failed"; + } + } + // End process if instance exists + emit QCoreApplication::quit(); +} + +void VsInit::connectionFailed(QLocalSocket::LocalSocketError socketError) +{ + switch (socketError) { + case QLocalSocket::ServerNotFoundError: + case QLocalSocket::SocketTimeoutError: + case QLocalSocket::ConnectionRefusedError: + m_instanceServer.reset(new QLocalServer(this)); + m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption); + m_instanceServer->listen("jacktripExists"); + QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this, + &VsInit::responseReceived, Qt::QueuedConnection); + break; + case QLocalSocket::PeerClosedError: + break; + default: + qDebug() << m_instanceCheckSocket->errorString(); + } +} + +void VsInit::responseReceived() +{ + // This is the first instance. Bring it to the top. + if (!m_vs.isNull() && m_vs->vsModeActive()) { + m_vs->raiseToTop(); + } + while (m_instanceServer->hasPendingConnections()) { + // Receive URL from 2nd instance + QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection(); + + if (!connectedSocket->waitForConnected()) { + qDebug() << "Never received connection"; + return; + } + + if (!connectedSocket->waitForReadyRead()) { + qDebug() << "Never ready to read"; + if (!(connectedSocket->bytesAvailable() > 0)) { + qDebug() << "Not ready and no bytes available"; + return; + } + } + + if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) { + qDebug() << "no bytes available"; + break; + } + + QByteArray in(connectedSocket->readAll()); + QString urlString(in); + QUrl url(urlString); + + // Join studio using received URL + if (!m_vs.isNull() && m_vs->vsModeActive() && url.scheme() == "jacktrip" + && url.host() == "join") { + m_vs->setStudioToJoin(url); + } + } +} diff --git a/src/gui/vsInit.h b/src/gui/vsInit.h new file mode 100644 index 0000000..cfe7b2f --- /dev/null +++ b/src/gui/vsInit.h @@ -0,0 +1,74 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. + SoundWIRE group at CCRMA, Stanford University. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsInit.h + * \author Aaron Wyatt, based on code by Matt Horton + * \date February 2023 + */ + +#ifndef __VSINIT_H__ +#define __VSINIT_H__ + +#include +#include +#include +#include + +#include "virtualstudio.h" + +class VsInit : public QObject +{ + Q_OBJECT + + public: + VsInit() = default; + + static QString parseDeeplink(QCoreApplication* app); + void checkForInstance(QString& deeplink); + void setVs(QSharedPointer vs) { m_vs = vs; } +#ifdef _WIN32 + static void setUrlScheme(); +#endif + + private slots: + void connectionReceived(); + void connectionFailed(QLocalSocket::LocalSocketError socketError); + void responseReceived(); + + private: + QScopedPointer m_instanceServer; + QScopedPointer m_instanceCheckSocket; + QSharedPointer m_vs; + QString m_deeplink; +}; + +#endif // __VSINIT_H__ diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp index 5657a44..4f6cdcd 100644 --- a/src/gui/vsServerInfo.cpp +++ b/src/gui/vsServerInfo.cpp @@ -181,14 +181,14 @@ void VsServerInfo::setRegion(const QString& region) m_region = region; } -bool VsServerInfo::isManageable() const +bool VsServerInfo::isManaged() const { - return m_isManageable; + return m_isManaged; } -void VsServerInfo::setIsManageable(bool isManageable) +void VsServerInfo::setIsManaged(bool isManaged) { - m_isManageable = isManageable; + m_isManaged = isManaged; } quint16 VsServerInfo::period() const diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h index 956d611..e1c945d 100644 --- a/src/gui/vsServerInfo.h +++ b/src/gui/vsServerInfo.h @@ -54,7 +54,8 @@ class VsServerInfo : public QObject Q_PROPERTY(QString flag READ flag CONSTANT) Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT) Q_PROPERTY(QString location READ location CONSTANT) - Q_PROPERTY(bool isManageable READ isManageable CONSTANT) + Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT) + Q_PROPERTY(bool isManaged READ isManaged CONSTANT) Q_PROPERTY(quint16 period READ period CONSTANT) Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT) Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT) @@ -93,8 +94,8 @@ class VsServerInfo : public QObject QString flag() const; QString location() const; void setRegion(const QString& region); - bool isManageable() const; - void setIsManageable(bool isManageable); + bool isManaged() const; + void setIsManaged(bool isManageable); quint16 period() const; void setPeriod(quint16 period); quint32 sampleRate() const; @@ -126,9 +127,9 @@ class VsServerInfo : public QObject bool m_enabled; bool m_owner; bool m_admin; + bool m_isManaged; bool m_isPublic; QString m_region; - bool m_isManageable; quint16 m_period; quint32 m_sampleRate; quint16 m_queueBuffer; @@ -143,7 +144,6 @@ class VsServerInfo : public QObject "loopback": true, "stereo": true, "type": "JackTrip", - "managed": true, "size": "c5.large", "mixBranch": "main", "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;", diff --git a/src/gui/vuMeter.cpp b/src/gui/vuMeter.cpp index 36736c7..1e19ea7 100644 --- a/src/gui/vuMeter.cpp +++ b/src/gui/vuMeter.cpp @@ -35,6 +35,9 @@ VuMeter::VuMeter(QWidget* parent) : QWidget(parent), m_level(0) m_yellowOff.setRgb(85, 65, 22); m_redOn.setRgb(242, 27, 27); m_redOff.setRgb(84, 4, 4); + + m_threshhold1 = std::round(m_bins * 0.6); + m_threshhold2 = std::round(m_bins * 0.8); } void VuMeter::setLevel(qreal level) @@ -53,17 +56,17 @@ void VuMeter::paintEvent([[maybe_unused]] QPaintEvent* event) for (quint32 i = 0; i < m_bins; i++) { bool on = level > i; if (on) { - if (i < 9) { + if (i < m_threshhold1) { painter.setBrush(m_greenOn); - } else if (i < 12) { + } else if (i < m_threshhold2) { painter.setBrush(m_yellowOn); } else { painter.setBrush(m_redOn); } } else { - if (i < 9) { + if (i < m_threshhold1) { painter.setBrush(m_greenOff); - } else if (i < 12) { + } else if (i < m_threshhold2) { painter.setBrush(m_yellowOff); } else { painter.setBrush(m_redOff); diff --git a/src/gui/vuMeter.h b/src/gui/vuMeter.h index 735dc14..d6d3693 100644 --- a/src/gui/vuMeter.h +++ b/src/gui/vuMeter.h @@ -51,8 +51,10 @@ class VuMeter : public QWidget QColor m_redOn; QColor m_redOff; - quint32 m_bins = 15; + quint32 m_bins = 30; quint32 m_margins = 2; + quint32 m_threshhold1; + quint32 m_threshhold2; }; #endif // VUMETER_H diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index 740b5ea..a1e4805 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "1.7.0"; ///< JackTrip version +constexpr const char* const gVersion = "1.8.1"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/main.cpp b/src/main.cpp index 9bf27f5..c561b2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,7 +37,6 @@ #ifndef NO_GUI #include -#include #ifndef NO_UPDATER #include "dblsqd/feed.h" @@ -47,8 +46,6 @@ #ifndef NO_VS #include #include -#include -#include #include #include #include @@ -56,6 +53,7 @@ #include "JTApplication.h" #include "gui/virtualstudio.h" +#include "gui/vsInit.h" #include "gui/vsQmlClipboard.h" #include "gui/vsUrlHandler.h" #endif @@ -285,11 +283,10 @@ int main(int argc, char* argv[]) QSharedPointer window; #ifndef NO_VS - QString deeplink = QLatin1String(""); + QString deeplink; QSharedPointer vs; #ifdef _WIN32 - QSharedPointer instanceServer; - QSharedPointer instanceCheckSocket; + QScopedPointer vsInit; #endif #endif @@ -311,135 +308,28 @@ int main(int argc, char* argv[]) app->setApplicationName(QStringLiteral("JackTrip")); app->setApplicationVersion(gVersion); - QCommandLineParser parser; - QCommandLineOption verboseOption(QStringList() << QStringLiteral("V") - << QStringLiteral("verbose")); - parser.addOption(verboseOption); - parser.parse(app->arguments()); - if (parser.isSet(verboseOption)) { - gVerboseFlag = true; - } + Settings cliSettings(true); + cliSettings.parseInput(argc, argv); #ifndef NO_VS // Register clipboard Qml type qmlRegisterType("VS", 1, 0, "Clipboard"); // Parse command line for deep link - QCommandLineOption deeplinkOption(QStringList() << QStringLiteral("deeplink")); - deeplinkOption.setValueName(QStringLiteral("deeplink")); - parser.addOption(deeplinkOption); - parser.parse(app->arguments()); - if (parser.isSet(deeplinkOption)) { - deeplink = parser.value(deeplinkOption); - } + deeplink = VsInit::parseDeeplink(app.data()); // Check if we need to show our first run window. QSettings settings; int uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); #ifdef _WIN32 // Set url scheme in registry - QString path = QDir::toNativeSeparators(qApp->applicationFilePath()); - - QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat); - set.beginGroup("jacktrip"); - set.setValue("Default", "URL:JackTrip Protocol"); - set.setValue("DefaultIcon/Default", path); - set.setValue("URL Protocol", ""); - set.setValue("shell/open/command/Default", - QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\""); - set.endGroup(); - - // Create socket - instanceCheckSocket = - QSharedPointer::create(new QLocalSocket(app.data())); - // End process if instance exists - QObject::connect( - instanceCheckSocket.data(), &QLocalSocket::connected, app.data(), - [&]() { - // pass deeplink to existing instance before quitting - if (!deeplink.isEmpty()) { - QByteArray baDeeplink = deeplink.toLocal8Bit(); - qint64 writeBytes = instanceCheckSocket->write(baDeeplink); - instanceCheckSocket->flush(); - instanceCheckSocket->disconnectFromServer(); // remove next - - if (writeBytes < 0) { - qDebug() << "sending deeplink failed"; - } - } - emit QCoreApplication::quit(); - }, - Qt::QueuedConnection); - // Create instanceServer to prevent new instances from being created - void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError); -#ifdef Q_OS_LINUX - errorFunc = &QLocalSocket::error; -#else - errorFunc = &QLocalSocket::errorOccurred; -#endif - QObject::connect( - instanceCheckSocket.data(), errorFunc, app.data(), - [&](QLocalSocket::LocalSocketError socketError) { - switch (socketError) { - case QLocalSocket::ServerNotFoundError: - case QLocalSocket::SocketTimeoutError: - case QLocalSocket::ConnectionRefusedError: - instanceServer = QSharedPointer::create( - new QLocalServer(app.data())); - instanceServer->setSocketOptions(QLocalServer::WorldAccessOption); - instanceServer->listen("jacktripExists"); - QObject::connect( - instanceServer.data(), &QLocalServer::newConnection, app.data(), - [&]() { - // This is the first instance. Bring it to the - // top. - vs->raiseToTop(); - while (instanceServer->hasPendingConnections()) { - // Receive URL from 2nd instance - QLocalSocket* connectedSocket = - instanceServer->nextPendingConnection(); - - if (!connectedSocket->waitForConnected()) { - qDebug() << "Never received connection"; - return; - } - - if (!connectedSocket->waitForReadyRead()) { - qDebug() << "Never ready to read"; - return; - } - - if (connectedSocket->bytesAvailable() - < (int)sizeof(quint16)) { - qDebug() << "no bytes available"; - break; - } - - QByteArray in(connectedSocket->readAll()); - QString urlString(in); - QUrl url(urlString); - - // Join studio using received URL - if (url.scheme() == "jacktrip" && url.host() == "join") { - vs->setStudioToJoin(url); - } - } - }, - Qt::QueuedConnection); - break; - case QLocalSocket::PeerClosedError: - break; - default: - qDebug() << instanceCheckSocket->errorString(); - } - }); - // Check for existing instance - instanceCheckSocket->connectToServer("jacktripExists"); - + VsInit::setUrlScheme(); + vsInit.reset(new VsInit()); + vsInit->checkForInstance(deeplink); #endif // _WIN32 - window.reset(new QJackTrip(argc, !deeplink.isEmpty())); + window.reset(new QJackTrip(&cliSettings, !deeplink.isEmpty())); #else - window.reset(new QJackTrip(argc)); + window.reset(new QJackTrip(&cliSettings)); #endif // NO_VS QObject::connect(window.data(), &QJackTrip::signalExit, app.data(), &QCoreApplication::quit, Qt::QueuedConnection); @@ -447,6 +337,9 @@ int main(int argc, char* argv[]) vs.reset(new VirtualStudio(uiMode == QJackTrip::UNSET)); QObject::connect(vs.data(), &VirtualStudio::signalExit, app.data(), &QCoreApplication::quit, Qt::QueuedConnection); +#ifdef _WIN32 + vsInit->setVs(vs); +#endif vs->setStandardWindow(window); window->setVs(vs); @@ -499,10 +392,8 @@ int main(int argc, char* argv[]) QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable") .toString() .toLower(); - QString baseUrl = - QStringLiteral( - "https://raw.githubusercontent.com/jacktrip/jacktrip/dev/releases/%1") - .arg(updateChannel); + QString baseUrl = QStringLiteral("https://files.jacktrip.org/app-releases/%1") + .arg(updateChannel); #else QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip"); #endif // PSI @@ -522,7 +413,9 @@ int main(int argc, char* argv[]) } else { #endif // NO_GUI // Otherwise use the non-GUI version, and parse our command line. +#ifndef PSI QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true")); +#endif try { Settings settings; settings.parseInput(argc, argv); diff --git a/src/stereotomonodsp.h b/src/stereotomonodsp.h new file mode 100644 index 0000000..40a1403 --- /dev/null +++ b/src/stereotomonodsp.h @@ -0,0 +1,2078 @@ +/* ------------------------------------------------------------ +author: "Dominick Hing" +license: "MIT Style STK-4.2" +name: "stereo-to-mono" +version: "1.0" +Code generated with Faust 2.50.6 (https://faust.grame.fr) +Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn stereotomonodsp -es 1 -mcd 16 -single -ftz 0 +------------------------------------------------------------ */ + +#ifndef __stereotomonodsp_H__ +#define __stereotomonodsp_H__ + +// NOTE: ANY INCLUDE-GUARD HERE MUST BE DERIVED FROM THE CLASS NAME +// +// faust2header.cpp - FAUST Architecture File +// This is a simple variation of matlabplot.cpp in the Faust distribution +// aimed at creating a simple C++ header file (.h) containing a Faust DSP. +// See the Makefile for how to use it. + +/************************** BEGIN dsp.h ******************************** + FAUST Architecture File + Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + EXCEPTION : As a special exception, you may create a larger work + that contains this FAUST architecture section and distribute + that work under terms of your choice, so long as this FAUST + architecture section is not modified. + ************************************************************************/ + +#ifndef __dsp__ +#define __dsp__ + +#include +#include + +/************************************************************************ + ************************************************************************ + FAUST compiler + Copyright (C) 2003-2018 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + ************************************************************************ + ************************************************************************/ + +#ifndef __export__ +#define __export__ + +#define FAUSTVERSION "2.50.6" + +// Use FAUST_API for code that is part of the external API but is also compiled in faust and libfaust +// Use LIBFAUST_API for code that is compiled in faust and libfaust + +#ifdef _WIN32 + #pragma warning (disable: 4251) + #ifdef FAUST_EXE + #define FAUST_API + #define LIBFAUST_API + #elif FAUST_LIB + #define FAUST_API __declspec(dllexport) + #define LIBFAUST_API __declspec(dllexport) + #else + #define FAUST_API + #define LIBFAUST_API + #endif +#else + #ifdef FAUST_EXE + #define FAUST_API + #define LIBFAUST_API + #else + #define FAUST_API __attribute__((visibility("default"))) + #define LIBFAUST_API __attribute__((visibility("default"))) + #endif +#endif + +#endif + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +struct FAUST_API UI; +struct FAUST_API Meta; + +/** + * DSP memory manager. + */ + +struct FAUST_API dsp_memory_manager { + + virtual ~dsp_memory_manager() {} + + /** + * Inform the Memory Manager with the number of expected memory zones. + * @param count - the number of expected memory zones + */ + virtual void begin(size_t /* count */) {} + + /** + * Give the Memory Manager information on a given memory zone. + * @param size - the size in bytes of the memory zone + * @param reads - the number of Read access to the zone used to compute one frame + * @param writes - the number of Write access to the zone used to compute one frame + */ + virtual void info(size_t /* size */, size_t /* reads */, size_t /* writes */) {} + + /** + * Inform the Memory Manager that all memory zones have been described, + * to possibly start a 'compute the best allocation strategy' step. + */ + virtual void end() {} + + /** + * Allocate a memory zone. + * @param size - the memory zone size in bytes + */ + virtual void* allocate(size_t size) = 0; + + /** + * Destroy a memory zone. + * @param ptr - the memory zone pointer to be deallocated + */ + virtual void destroy(void* ptr) = 0; + +}; + +/** +* Signal processor definition. +*/ + +class FAUST_API dsp { + + public: + + dsp() {} + virtual ~dsp() {} + + /* Return instance number of audio inputs */ + virtual int getNumInputs() = 0; + + /* Return instance number of audio outputs */ + virtual int getNumOutputs() = 0; + + /** + * Trigger the ui_interface parameter with instance specific calls + * to 'openTabBox', 'addButton', 'addVerticalSlider'... in order to build the UI. + * + * @param ui_interface - the user interface builder + */ + virtual void buildUserInterface(UI* ui_interface) = 0; + + /* Return the sample rate currently used by the instance */ + virtual int getSampleRate() = 0; + + /** + * Global init, calls the following methods: + * - static class 'classInit': static tables initialization + * - 'instanceInit': constants and instance state initialization + * + * @param sample_rate - the sampling rate in Hz + */ + virtual void init(int sample_rate) = 0; + + /** + * Init instance state + * + * @param sample_rate - the sampling rate in Hz + */ + virtual void instanceInit(int sample_rate) = 0; + + /** + * Init instance constant state + * + * @param sample_rate - the sampling rate in Hz + */ + virtual void instanceConstants(int sample_rate) = 0; + + /* Init default control parameters values */ + virtual void instanceResetUserInterface() = 0; + + /* Init instance state (like delay lines...) but keep the control parameter values */ + virtual void instanceClear() = 0; + + /** + * Return a clone of the instance. + * + * @return a copy of the instance on success, otherwise a null pointer. + */ + virtual dsp* clone() = 0; + + /** + * Trigger the Meta* parameter with instance specific calls to 'declare' (key, value) metadata. + * + * @param m - the Meta* meta user + */ + virtual void metadata(Meta* m) = 0; + + /** + * DSP instance computation, to be called with successive in/out audio buffers. + * + * @param count - the number of frames to compute + * @param inputs - the input audio buffers as an array of non-interleaved FAUSTFLOAT samples (eiher float, double or quad) + * @param outputs - the output audio buffers as an array of non-interleaved FAUSTFLOAT samples (eiher float, double or quad) + * + */ + virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) = 0; + + /** + * DSP instance computation: alternative method to be used by subclasses. + * + * @param date_usec - the timestamp in microsec given by audio driver. + * @param count - the number of frames to compute + * @param inputs - the input audio buffers as an array of non-interleaved FAUSTFLOAT samples (either float, double or quad) + * @param outputs - the output audio buffers as an array of non-interleaved FAUSTFLOAT samples (either float, double or quad) + * + */ + virtual void compute(double /*date_usec*/, int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { compute(count, inputs, outputs); } + +}; + +/** + * Generic DSP decorator. + */ + +class FAUST_API decorator_dsp : public dsp { + + protected: + + dsp* fDSP; + + public: + + decorator_dsp(dsp* dsp = nullptr):fDSP(dsp) {} + virtual ~decorator_dsp() { delete fDSP; } + + virtual int getNumInputs() { return fDSP->getNumInputs(); } + virtual int getNumOutputs() { return fDSP->getNumOutputs(); } + virtual void buildUserInterface(UI* ui_interface) { fDSP->buildUserInterface(ui_interface); } + virtual int getSampleRate() { return fDSP->getSampleRate(); } + virtual void init(int sample_rate) { fDSP->init(sample_rate); } + virtual void instanceInit(int sample_rate) { fDSP->instanceInit(sample_rate); } + virtual void instanceConstants(int sample_rate) { fDSP->instanceConstants(sample_rate); } + virtual void instanceResetUserInterface() { fDSP->instanceResetUserInterface(); } + virtual void instanceClear() { fDSP->instanceClear(); } + virtual decorator_dsp* clone() { return new decorator_dsp(fDSP->clone()); } + virtual void metadata(Meta* m) { fDSP->metadata(m); } + // Beware: subclasses usually have to overload the two 'compute' methods + virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { fDSP->compute(count, inputs, outputs); } + virtual void compute(double date_usec, int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { fDSP->compute(date_usec, count, inputs, outputs); } + +}; + +/** + * DSP factory class, used with LLVM and Interpreter backends + * to create DSP instances from a compiled DSP program. + */ + +class FAUST_API dsp_factory { + + protected: + + // So that to force sub-classes to use deleteDSPFactory(dsp_factory* factory); + virtual ~dsp_factory() {} + + public: + + virtual std::string getName() = 0; + virtual std::string getSHAKey() = 0; + virtual std::string getDSPCode() = 0; + virtual std::string getCompileOptions() = 0; + virtual std::vector getLibraryList() = 0; + virtual std::vector getIncludePathnames() = 0; + + virtual dsp* createDSPInstance() = 0; + + virtual void setMemoryManager(dsp_memory_manager* manager) = 0; + virtual dsp_memory_manager* getMemoryManager() = 0; + +}; + +// Denormal handling + +#if defined (__SSE__) +#include +#endif + +class FAUST_API ScopedNoDenormals { + + private: + + intptr_t fpsr = 0; + + void setFpStatusRegister(intptr_t fpsr_aux) noexcept + { + #if defined (__arm64__) || defined (__aarch64__) + asm volatile("msr fpcr, %0" : : "ri" (fpsr_aux)); + #elif defined (__SSE__) + // The volatile keyword here is needed to workaround a bug in AppleClang 13.0 + // which aggressively optimises away the variable otherwise + volatile uint32_t fpsr_w = static_cast(fpsr_aux); + _mm_setcsr(fpsr_w); + #endif + } + + void getFpStatusRegister() noexcept + { + #if defined (__arm64__) || defined (__aarch64__) + asm volatile("mrs %0, fpcr" : "=r" (fpsr)); + #elif defined (__SSE__) + fpsr = static_cast(_mm_getcsr()); + #endif + } + + public: + + ScopedNoDenormals() noexcept + { + #if defined (__arm64__) || defined (__aarch64__) + intptr_t mask = (1 << 24 /* FZ */); + #elif defined (__SSE__) + #if defined (__SSE2__) + intptr_t mask = 0x8040; + #else + intptr_t mask = 0x8000; + #endif + #else + intptr_t mask = 0x0000; + #endif + getFpStatusRegister(); + setFpStatusRegister(fpsr | mask); + } + + ~ScopedNoDenormals() noexcept + { + setFpStatusRegister(fpsr); + } + +}; + +#define AVOIDDENORMALS ScopedNoDenormals ftz_scope; + +#endif + +/************************** END dsp.h **************************/ +/************************** BEGIN APIUI.h ***************************** +FAUST Architecture File +Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale +--------------------------------------------------------------------- +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +EXCEPTION : As a special exception, you may create a larger work +that contains this FAUST architecture section and distribute +that work under terms of your choice, so long as this FAUST +architecture section is not modified. +************************************************************************/ + +#ifndef API_UI_H +#define API_UI_H + +#include +#include +#include +#include +#include + +/************************** BEGIN meta.h ******************************* + FAUST Architecture File + Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + EXCEPTION : As a special exception, you may create a larger work + that contains this FAUST architecture section and distribute + that work under terms of your choice, so long as this FAUST + architecture section is not modified. + ************************************************************************/ + +#ifndef __meta__ +#define __meta__ + + +/** + The base class of Meta handler to be used in dsp::metadata(Meta* m) method to retrieve (key, value) metadata. + */ +struct FAUST_API Meta { + virtual ~Meta() {} + virtual void declare(const char* key, const char* value) = 0; +}; + +#endif +/************************** END meta.h **************************/ +/************************** BEGIN UI.h ***************************** + FAUST Architecture File + Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + EXCEPTION : As a special exception, you may create a larger work + that contains this FAUST architecture section and distribute + that work under terms of your choice, so long as this FAUST + architecture section is not modified. + ********************************************************************/ + +#ifndef __UI_H__ +#define __UI_H__ + + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +/******************************************************************************* + * UI : Faust DSP User Interface + * User Interface as expected by the buildUserInterface() method of a DSP. + * This abstract class contains only the method that the Faust compiler can + * generate to describe a DSP user interface. + ******************************************************************************/ + +struct Soundfile; + +template +struct FAUST_API UIReal { + + UIReal() {} + virtual ~UIReal() {} + + // -- widget's layouts + + virtual void openTabBox(const char* label) = 0; + virtual void openHorizontalBox(const char* label) = 0; + virtual void openVerticalBox(const char* label) = 0; + virtual void closeBox() = 0; + + // -- active widgets + + virtual void addButton(const char* label, REAL* zone) = 0; + virtual void addCheckButton(const char* label, REAL* zone) = 0; + virtual void addVerticalSlider(const char* label, REAL* zone, REAL init, REAL min, REAL max, REAL step) = 0; + virtual void addHorizontalSlider(const char* label, REAL* zone, REAL init, REAL min, REAL max, REAL step) = 0; + virtual void addNumEntry(const char* label, REAL* zone, REAL init, REAL min, REAL max, REAL step) = 0; + + // -- passive widgets + + virtual void addHorizontalBargraph(const char* label, REAL* zone, REAL min, REAL max) = 0; + virtual void addVerticalBargraph(const char* label, REAL* zone, REAL min, REAL max) = 0; + + // -- soundfiles + + virtual void addSoundfile(const char* label, const char* filename, Soundfile** sf_zone) = 0; + + // -- metadata declarations + + virtual void declare(REAL* /* zone */, const char* /* key */, const char* /* val */) {} + + // To be used by LLVM client + virtual int sizeOfFAUSTFLOAT() { return sizeof(FAUSTFLOAT); } +}; + +struct FAUST_API UI : public UIReal { + UI() {} + virtual ~UI() {} +}; + +#endif +/************************** END UI.h **************************/ +/************************** BEGIN PathBuilder.h ************************** + FAUST Architecture File + Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + EXCEPTION : As a special exception, you may create a larger work + that contains this FAUST architecture section and distribute + that work under terms of your choice, so long as this FAUST + architecture section is not modified. + ************************************************************************/ + +#ifndef __PathBuilder__ +#define __PathBuilder__ + +#include +#include +#include +#include +#include +#include + + +/******************************************************************************* + * PathBuilder : Faust User Interface + * Helper class to build complete hierarchical path for UI items. + ******************************************************************************/ + +class FAUST_API PathBuilder { + + protected: + + std::vector fControlsLevel; + std::vector fFullPaths; + std::map fFull2Short; // filled by computeShortNames() + + /** + * @brief check if a character is acceptable for an ID + * + * @param c + * @return true is the character is acceptable for an ID + */ + bool isIDChar(char c) const + { + return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')); + } + + /** + * @brief remove all "/0x00" parts + * + * @param src + * @return modified string + */ + std::string remove0x00(const std::string& src) const + { + return std::regex_replace(src, std::regex("/0x00"), ""); + } + + /** + * @brief replace all non ID char with '_' (one '_' may replace several non ID char) + * + * @param src + * @return modified string + */ + std::string str2ID(const std::string& src) const + { + std::string dst; + bool need_underscore = false; + for (char c : src) { + if (isIDChar(c) || (c == '/')) { + if (need_underscore) { + dst.push_back('_'); + need_underscore = false; + } + dst.push_back(c); + } else { + need_underscore = true; + } + } + return dst; + } + + /** + * @brief Keep only the last n slash-parts + * + * @param src + * @param n : 1 indicates the last slash-part + * @return modified string + */ + std::string cut(const std::string& src, int n) const + { + std::string rdst; + for (int i = int(src.length())-1; i >= 0; i--) { + char c = src[i]; + if (c != '/') { + rdst.push_back(c); + } else if (n == 1) { + std::string dst; + for (int j = int(rdst.length())-1; j >= 0; j--) { + dst.push_back(rdst[j]); + } + return dst; + } else { + n--; + rdst.push_back(c); + } + } + return src; + } + + void addFullPath(const std::string& label) { fFullPaths.push_back(buildPath(label)); } + + /** + * @brief Compute the mapping between full path and short names + */ + void computeShortNames() + { + std::vector uniquePaths; // all full paths transformed but made unique with a prefix + std::map unique2full; // all full paths transformed but made unique with a prefix + char num_buffer[16]; + int pnum = 0; + + for (const auto& s : fFullPaths) { + sprintf(num_buffer, "%d", pnum++); + std::string u = "/P" + std::string(num_buffer) + str2ID(remove0x00(s)); + uniquePaths.push_back(u); + unique2full[u] = s; // remember the full path associated to a unique path + } + + std::map uniquePath2level; // map path to level + for (const auto& s : uniquePaths) uniquePath2level[s] = 1; // we init all levels to 1 + bool have_collisions = true; + + while (have_collisions) { + // compute collision list + std::set collisionSet; + std::map short2full; + have_collisions = false; + for (const auto& it : uniquePath2level) { + std::string u = it.first; + int n = it.second; + std::string shortName = cut(u, n); + auto p = short2full.find(shortName); + if (p == short2full.end()) { + // no collision + short2full[shortName] = u; + } else { + // we have a collision, add the two paths to the collision set + have_collisions = true; + collisionSet.insert(u); + collisionSet.insert(p->second); + } + } + for (const auto& s : collisionSet) uniquePath2level[s]++; // increase level of colliding path + } + + for (const auto& it : uniquePath2level) { + std::string u = it.first; + int n = it.second; + std::string shortName = replaceCharList(cut(u, n), {'/'}, '_'); + fFull2Short[unique2full[u]] = shortName; + } + } + + std::string replaceCharList(const std::string& str, const std::vector& ch1, char ch2) + { + auto beg = ch1.begin(); + auto end = ch1.end(); + std::string res = str; + for (size_t i = 0; i < str.length(); ++i) { + if (std::find(beg, end, str[i]) != end) res[i] = ch2; + } + return res; + } + + public: + + PathBuilder() {} + virtual ~PathBuilder() {} + + // Return true for the first level of groups + bool pushLabel(const std::string& label) { fControlsLevel.push_back(label); return fControlsLevel.size() == 1;} + + // Return true for the last level of groups + bool popLabel() { fControlsLevel.pop_back(); return fControlsLevel.size() == 0; } + + std::string buildPath(const std::string& label) + { + std::string res = "/"; + for (size_t i = 0; i < fControlsLevel.size(); i++) { + res = res + fControlsLevel[i] + "/"; + } + res += label; + return replaceCharList(res, {' ', '#', '*', ',', '?', '[', ']', '{', '}', '(', ')'}, '_'); + } + +}; + +#endif // __PathBuilder__ +/************************** END PathBuilder.h **************************/ +/************************** BEGIN ValueConverter.h ******************** + FAUST Architecture File + Copyright (C) 2003-2022 GRAME, Centre National de Creation Musicale + --------------------------------------------------------------------- + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + EXCEPTION : As a special exception, you may create a larger work + that contains this FAUST architecture section and distribute + that work under terms of your choice, so long as this FAUST + architecture section is not modified. + ********************************************************************/ + +#ifndef __ValueConverter__ +#define __ValueConverter__ + +/*************************************************************************************** + ValueConverter.h + (GRAME, Copyright 2015-2019) + + Set of conversion objects used to map user interface values (for example a gui slider + delivering values between 0 and 1) to faust values (for example a vslider between + 20 and 20000) using a log scale. + + -- Utilities + + Range(lo,hi) : clip a value x between lo and hi + Interpolator(lo,hi,v1,v2) : Maps a value x between lo and hi to a value y between v1 and v2 + Interpolator3pt(lo,mi,hi,v1,vm,v2) : Map values between lo mid hi to values between v1 vm v2 + + -- Value Converters + + ValueConverter::ui2faust(x) + ValueConverter::faust2ui(x) + + -- ValueConverters used for sliders depending of the scale + + LinearValueConverter(umin, umax, fmin, fmax) + LinearValueConverter2(lo, mi, hi, v1, vm, v2) using 2 segments + LogValueConverter(umin, umax, fmin, fmax) + ExpValueConverter(umin, umax, fmin, fmax) + + -- ValueConverters used for accelerometers based on 3 points + + AccUpConverter(amin, amid, amax, fmin, fmid, fmax) -- curve 0 + AccDownConverter(amin, amid, amax, fmin, fmid, fmax) -- curve 1 + AccUpDownConverter(amin, amid, amax, fmin, fmid, fmax) -- curve 2 + AccDownUpConverter(amin, amid, amax, fmin, fmid, fmax) -- curve 3 + + -- lists of ZoneControl are used to implement accelerometers metadata for each axes + + ZoneControl(zone, valueConverter) : a zone with an accelerometer data converter + + -- ZoneReader are used to implement screencolor metadata + + ZoneReader(zone, valueConverter) : a zone with a data converter + +****************************************************************************************/ + +#include +#include // std::max +#include +#include +#include + + +//-------------------------------------------------------------------------------------- +// Interpolator(lo,hi,v1,v2) +// Maps a value x between lo and hi to a value y between v1 and v2 +// y = v1 + (x-lo)/(hi-lo)*(v2-v1) +// y = v1 + (x-lo) * coef with coef = (v2-v1)/(hi-lo) +// y = v1 + x*coef - lo*coef +// y = v1 - lo*coef + x*coef +// y = offset + x*coef with offset = v1 - lo*coef +//-------------------------------------------------------------------------------------- +class FAUST_API Interpolator { + + private: + + //-------------------------------------------------------------------------------------- + // Range(lo,hi) clip a value between lo and hi + //-------------------------------------------------------------------------------------- + struct Range + { + double fLo; + double fHi; + + Range(double x, double y) : fLo(std::min(x,y)), fHi(std::max(x,y)) {} + double operator()(double x) { return (xfHi) ? fHi : x; } + }; + + + Range fRange; + double fCoef; + double fOffset; + + public: + + Interpolator(double lo, double hi, double v1, double v2) : fRange(lo,hi) + { + if (hi != lo) { + // regular case + fCoef = (v2-v1)/(hi-lo); + fOffset = v1 - lo*fCoef; + } else { + // degenerate case, avoids division by zero + fCoef = 0; + fOffset = (v1+v2)/2; + } + } + double operator()(double v) + { + double x = fRange(v); + return fOffset + x*fCoef; + } + + void getLowHigh(double& amin, double& amax) + { + amin = fRange.fLo; + amax = fRange.fHi; + } +}; + +//-------------------------------------------------------------------------------------- +// Interpolator3pt(lo,mi,hi,v1,vm,v2) +// Map values between lo mid hi to values between v1 vm v2 +//-------------------------------------------------------------------------------------- +class FAUST_API Interpolator3pt { + + private: + + Interpolator fSegment1; + Interpolator fSegment2; + double fMid; + + public: + + Interpolator3pt(double lo, double mi, double hi, double v1, double vm, double v2) : + fSegment1(lo, mi, v1, vm), + fSegment2(mi, hi, vm, v2), + fMid(mi) {} + double operator()(double x) { return (x < fMid) ? fSegment1(x) : fSegment2(x); } + + void getMappingValues(double& amin, double& amid, double& amax) + { + fSegment1.getLowHigh(amin, amid); + fSegment2.getLowHigh(amid, amax); + } +}; + +//-------------------------------------------------------------------------------------- +// Abstract ValueConverter class. Converts values between UI and Faust representations +//-------------------------------------------------------------------------------------- +class FAUST_API ValueConverter { + + public: + + virtual ~ValueConverter() {} + virtual double ui2faust(double x) { return x; }; + virtual double faust2ui(double x) { return x; }; +}; + +//-------------------------------------------------------------------------------------- +// A converter than can be updated +//-------------------------------------------------------------------------------------- + +class FAUST_API UpdatableValueConverter : public ValueConverter { + + protected: + + bool fActive; + + public: + + UpdatableValueConverter():fActive(true) + {} + virtual ~UpdatableValueConverter() + {} + + virtual void setMappingValues(double amin, double amid, double amax, double min, double init, double max) = 0; + virtual void getMappingValues(double& amin, double& amid, double& amax) = 0; + + void setActive(bool on_off) { fActive = on_off; } + bool getActive() { return fActive; } + +}; + +//-------------------------------------------------------------------------------------- +// Linear conversion between ui and Faust values +//-------------------------------------------------------------------------------------- +class FAUST_API LinearValueConverter : public ValueConverter { + + private: + + Interpolator fUI2F; + Interpolator fF2UI; + + public: + + LinearValueConverter(double umin, double umax, double fmin, double fmax) : + fUI2F(umin,umax,fmin,fmax), fF2UI(fmin,fmax,umin,umax) + {} + + LinearValueConverter() : fUI2F(0.,0.,0.,0.), fF2UI(0.,0.,0.,0.) + {} + virtual double ui2faust(double x) { return fUI2F(x); } + virtual double faust2ui(double x) { return fF2UI(x); } + +}; + +//-------------------------------------------------------------------------------------- +// Two segments linear conversion between ui and Faust values +//-------------------------------------------------------------------------------------- +class FAUST_API LinearValueConverter2 : public UpdatableValueConverter { + + private: + + Interpolator3pt fUI2F; + Interpolator3pt fF2UI; + + public: + + LinearValueConverter2(double amin, double amid, double amax, double min, double init, double max) : + fUI2F(amin, amid, amax, min, init, max), fF2UI(min, init, max, amin, amid, amax) + {} + + LinearValueConverter2() : fUI2F(0.,0.,0.,0.,0.,0.), fF2UI(0.,0.,0.,0.,0.,0.) + {} + + virtual double ui2faust(double x) { return fUI2F(x); } + virtual double faust2ui(double x) { return fF2UI(x); } + + virtual void setMappingValues(double amin, double amid, double amax, double min, double init, double max) + { + fUI2F = Interpolator3pt(amin, amid, amax, min, init, max); + fF2UI = Interpolator3pt(min, init, max, amin, amid, amax); + } + + virtual void getMappingValues(double& amin, double& amid, double& amax) + { + fUI2F.getMappingValues(amin, amid, amax); + } + +}; + +//-------------------------------------------------------------------------------------- +// Logarithmic conversion between ui and Faust values +//-------------------------------------------------------------------------------------- +class FAUST_API LogValueConverter : public LinearValueConverter { + + public: + + LogValueConverter(double umin, double umax, double fmin, double fmax) : + LinearValueConverter(umin, umax, std::log(std::max(DBL_MIN, fmin)), std::log(std::max(DBL_MIN, fmax))) + {} + + virtual double ui2faust(double x) { return std::exp(LinearValueConverter::ui2faust(x)); } + virtual double faust2ui(double x) { return LinearValueConverter::faust2ui(std::log(std::max(x, DBL_MIN))); } + +}; + +//-------------------------------------------------------------------------------------- +// Exponential conversion between ui and Faust values +//-------------------------------------------------------------------------------------- +class FAUST_API ExpValueConverter : public LinearValueConverter { + + public: + + ExpValueConverter(double umin, double umax, double fmin, double fmax) : + LinearValueConverter(umin, umax, std::min(DBL_MAX, std::exp(fmin)), std::min(DBL_MAX, std::exp(fmax))) + {} + + virtual double ui2faust(double x) { return std::log(LinearValueConverter::ui2faust(x)); } + virtual double faust2ui(double x) { return LinearValueConverter::faust2ui(std::min(DBL_MAX, std::exp(x))); } + +}; + +//-------------------------------------------------------------------------------------- +// Convert accelerometer or gyroscope values to Faust values +// Using an Up curve (curve 0) +//-------------------------------------------------------------------------------------- +class FAUST_API AccUpConverter : public UpdatableValueConverter { + + private: + + Interpolator3pt fA2F; + Interpolator3pt fF2A; + + public: + + AccUpConverter(double amin, double amid, double amax, double fmin, double fmid, double fmax) : + fA2F(amin,amid,amax,fmin,fmid,fmax), + fF2A(fmin,fmid,fmax,amin,amid,amax) + {} + + virtual double ui2faust(double x) { return fA2F(x); } + virtual double faust2ui(double x) { return fF2A(x); } + + virtual void setMappingValues(double amin, double amid, double amax, double fmin, double fmid, double fmax) + { + //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccUpConverter update %f %f %f %f %f %f", amin,amid,amax,fmin,fmid,fmax); + fA2F = Interpolator3pt(amin, amid, amax, fmin, fmid, fmax); + fF2A = Interpolator3pt(fmin, fmid, fmax, amin, amid, amax); + } + + virtual void getMappingValues(double& amin, double& amid, double& amax) + { + fA2F.getMappingValues(amin, amid, amax); + } + +}; + +//-------------------------------------------------------------------------------------- +// Convert accelerometer or gyroscope values to Faust values +// Using a Down curve (curve 1) +//-------------------------------------------------------------------------------------- +class FAUST_API AccDownConverter : public UpdatableValueConverter { + + private: + + Interpolator3pt fA2F; + Interpolator3pt fF2A; + + public: + + AccDownConverter(double amin, double amid, double amax, double fmin, double fmid, double fmax) : + fA2F(amin,amid,amax,fmax,fmid,fmin), + fF2A(fmin,fmid,fmax,amax,amid,amin) + {} + + virtual double ui2faust(double x) { return fA2F(x); } + virtual double faust2ui(double x) { return fF2A(x); } + + virtual void setMappingValues(double amin, double amid, double amax, double fmin, double fmid, double fmax) + { + //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccDownConverter update %f %f %f %f %f %f", amin,amid,amax,fmin,fmid,fmax); + fA2F = Interpolator3pt(amin, amid, amax, fmax, fmid, fmin); + fF2A = Interpolator3pt(fmin, fmid, fmax, amax, amid, amin); + } + + virtual void getMappingValues(double& amin, double& amid, double& amax) + { + fA2F.getMappingValues(amin, amid, amax); + } +}; + +//-------------------------------------------------------------------------------------- +// Convert accelerometer or gyroscope values to Faust values +// Using an Up-Down curve (curve 2) +//-------------------------------------------------------------------------------------- +class FAUST_API AccUpDownConverter : public UpdatableValueConverter { + + private: + + Interpolator3pt fA2F; + Interpolator fF2A; + + public: + + AccUpDownConverter(double amin, double amid, double amax, double fmin, double /* fmid */, double fmax) : + fA2F(amin,amid,amax,fmin,fmax,fmin), + fF2A(fmin,fmax,amin,amax) // Special, pseudo inverse of a non monotonic function + {} + + virtual double ui2faust(double x) { return fA2F(x); } + virtual double faust2ui(double x) { return fF2A(x); } + + virtual void setMappingValues(double amin, double amid, double amax, double fmin, double /* fmid */, double fmax) + { + //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccUpDownConverter update %f %f %f %f %f %f", amin,amid,amax,fmin,fmid,fmax); + fA2F = Interpolator3pt(amin, amid, amax, fmin, fmax, fmin); + fF2A = Interpolator(fmin, fmax, amin, amax); + } + + virtual void getMappingValues(double& amin, double& amid, double& amax) + { + fA2F.getMappingValues(amin, amid, amax); + } +}; + +//-------------------------------------------------------------------------------------- +// Convert accelerometer or gyroscope values to Faust values +// Using a Down-Up curve (curve 3) +//-------------------------------------------------------------------------------------- +class FAUST_API AccDownUpConverter : public UpdatableValueConverter { + + private: + + Interpolator3pt fA2F; + Interpolator fF2A; + + public: + + AccDownUpConverter(double amin, double amid, double amax, double fmin, double /* fmid */, double fmax) : + fA2F(amin,amid,amax,fmax,fmin,fmax), + fF2A(fmin,fmax,amin,amax) // Special, pseudo inverse of a non monotonic function + {} + + virtual double ui2faust(double x) { return fA2F(x); } + virtual double faust2ui(double x) { return fF2A(x); } + + virtual void setMappingValues(double amin, double amid, double amax, double fmin, double /* fmid */, double fmax) + { + //__android_log_print(ANDROID_LOG_ERROR, "Faust", "AccDownUpConverter update %f %f %f %f %f %f", amin,amid,amax,fmin,fmid,fmax); + fA2F = Interpolator3pt(amin, amid, amax, fmax, fmin, fmax); + fF2A = Interpolator(fmin, fmax, amin, amax); + } + + virtual void getMappingValues(double& amin, double& amid, double& amax) + { + fA2F.getMappingValues(amin, amid, amax); + } +}; + +//-------------------------------------------------------------------------------------- +// Base class for ZoneControl +//-------------------------------------------------------------------------------------- +class FAUST_API ZoneControl { + + protected: + + FAUSTFLOAT* fZone; + + public: + + ZoneControl(FAUSTFLOAT* zone) : fZone(zone) {} + virtual ~ZoneControl() {} + + virtual void update(double /* v */) const {} + + virtual void setMappingValues(int /* curve */, double /* amin */, double /* amid */, double /* amax */, double /* min */, double /* init */, double /* max */) {} + virtual void getMappingValues(double& /* amin */, double& /* amid */, double& /* amax */) {} + + FAUSTFLOAT* getZone() { return fZone; } + + virtual void setActive(bool /* on_off */) {} + virtual bool getActive() { return false; } + + virtual int getCurve() { return -1; } + +}; + +//-------------------------------------------------------------------------------------- +// Useful to implement accelerometers metadata as a list of ZoneControl for each axes +//-------------------------------------------------------------------------------------- +class FAUST_API ConverterZoneControl : public ZoneControl { + + protected: + + ValueConverter* fValueConverter; + + public: + + ConverterZoneControl(FAUSTFLOAT* zone, ValueConverter* converter) : ZoneControl(zone), fValueConverter(converter) {} + virtual ~ConverterZoneControl() { delete fValueConverter; } // Assuming fValueConverter is not kept elsewhere... + + virtual void update(double v) const { *fZone = FAUSTFLOAT(fValueConverter->ui2faust(v)); } + + ValueConverter* getConverter() { return fValueConverter; } + +}; + +//-------------------------------------------------------------------------------------- +// Association of a zone and a four value converter, each one for each possible curve. +// Useful to implement accelerometers metadata as a list of ZoneControl for each axes +//-------------------------------------------------------------------------------------- +class FAUST_API CurveZoneControl : public ZoneControl { + + private: + + std::vector fValueConverters; + int fCurve; + + public: + + CurveZoneControl(FAUSTFLOAT* zone, int curve, double amin, double amid, double amax, double min, double init, double max) : ZoneControl(zone), fCurve(0) + { + assert(curve >= 0 && curve <= 3); + fValueConverters.push_back(new AccUpConverter(amin, amid, amax, min, init, max)); + fValueConverters.push_back(new AccDownConverter(amin, amid, amax, min, init, max)); + fValueConverters.push_back(new AccUpDownConverter(amin, amid, amax, min, init, max)); + fValueConverters.push_back(new AccDownUpConverter(amin, amid, amax, min, init, max)); + fCurve = curve; + } + virtual ~CurveZoneControl() + { + for (const auto& it : fValueConverters) { delete it; } + } + void update(double v) const { if (fValueConverters[fCurve]->getActive()) *fZone = FAUSTFLOAT(fValueConverters[fCurve]->ui2faust(v)); } + + void setMappingValues(int curve, double amin, double amid, double amax, double min, double init, double max) + { + fValueConverters[curve]->setMappingValues(amin, amid, amax, min, init, max); + fCurve = curve; + } + + void getMappingValues(double& amin, double& amid, double& amax) + { + fValueConverters[fCurve]->getMappingValues(amin, amid, amax); + } + + void setActive(bool on_off) + { + for (const auto& it : fValueConverters) { it->setActive(on_off); } + } + + int getCurve() { return fCurve; } +}; + +class FAUST_API ZoneReader { + + private: + + FAUSTFLOAT* fZone; + Interpolator fInterpolator; + + public: + + ZoneReader(FAUSTFLOAT* zone, double lo, double hi) : fZone(zone), fInterpolator(lo, hi, 0, 255) {} + + virtual ~ZoneReader() {} + + int getValue() + { + return (fZone != nullptr) ? int(fInterpolator(*fZone)) : 127; + } + +}; + +#endif +/************************** END ValueConverter.h **************************/ + +typedef unsigned int uint; + +class APIUI : public PathBuilder, public Meta, public UI +{ + public: + enum ItemType { kButton = 0, kCheckButton, kVSlider, kHSlider, kNumEntry, kHBargraph, kVBargraph }; + enum Type { kAcc = 0, kGyr = 1, kNoType }; + + protected: + + enum Mapping { kLin = 0, kLog = 1, kExp = 2 }; + + struct Item { + std::string fLabel; + std::string fShortname; + std::string fPath; + ValueConverter* fConversion; + FAUSTFLOAT* fZone; + FAUSTFLOAT fInit; + FAUSTFLOAT fMin; + FAUSTFLOAT fMax; + FAUSTFLOAT fStep; + ItemType fItemType; + + Item(const std::string& label, + const std::string& short_name, + const std::string& path, + ValueConverter* conversion, + FAUSTFLOAT* zone, + FAUSTFLOAT init, + FAUSTFLOAT min, + FAUSTFLOAT max, + FAUSTFLOAT step, + ItemType item_type) + :fLabel(label), fShortname(short_name), fPath(path), fConversion(conversion), + fZone(zone), fInit(init), fMin(min), fMax(max), fStep(step), fItemType(item_type) + {} + }; + std::vector fItems; + + std::vector > fMetaData; + std::vector fAcc[3]; + std::vector fGyr[3]; + + // Screen color control + // "...[screencolor:red]..." etc. + bool fHasScreenControl; // true if control screen color metadata + ZoneReader* fRedReader; + ZoneReader* fGreenReader; + ZoneReader* fBlueReader; + + // Current values controlled by metadata + std::string fCurrentUnit; + int fCurrentScale; + std::string fCurrentAcc; + std::string fCurrentGyr; + std::string fCurrentColor; + std::string fCurrentTooltip; + std::map fCurrentMetadata; + + // Add a generic parameter + virtual void addParameter(const char* label, + FAUSTFLOAT* zone, + FAUSTFLOAT init, + FAUSTFLOAT min, + FAUSTFLOAT max, + FAUSTFLOAT step, + ItemType type) + { + std::string path = buildPath(label); + fFullPaths.push_back(path); + + // handle scale metadata + ValueConverter* converter = nullptr; + switch (fCurrentScale) { + case kLin: + converter = new LinearValueConverter(0, 1, min, max); + break; + case kLog: + converter = new LogValueConverter(0, 1, min, max); + break; + case kExp: + converter = new ExpValueConverter(0, 1, min, max); + break; + } + fCurrentScale = kLin; + + fItems.push_back(Item(label, "", path, converter, zone, init, min, max, step, type)); + + if (fCurrentAcc.size() > 0 && fCurrentGyr.size() > 0) { + fprintf(stderr, "warning : 'acc' and 'gyr' metadata used for the same %s parameter !!\n", label); + } + + // handle acc metadata "...[acc : ]..." + if (fCurrentAcc.size() > 0) { + std::istringstream iss(fCurrentAcc); + int axe, curve; + double amin, amid, amax; + iss >> axe >> curve >> amin >> amid >> amax; + + if ((0 <= axe) && (axe < 3) && + (0 <= curve) && (curve < 4) && + (amin < amax) && (amin <= amid) && (amid <= amax)) + { + fAcc[axe].push_back(new CurveZoneControl(zone, curve, amin, amid, amax, min, init, max)); + } else { + fprintf(stderr, "incorrect acc metadata : %s \n", fCurrentAcc.c_str()); + } + fCurrentAcc = ""; + } + + // handle gyr metadata "...[gyr : ]..." + if (fCurrentGyr.size() > 0) { + std::istringstream iss(fCurrentGyr); + int axe, curve; + double amin, amid, amax; + iss >> axe >> curve >> amin >> amid >> amax; + + if ((0 <= axe) && (axe < 3) && + (0 <= curve) && (curve < 4) && + (amin < amax) && (amin <= amid) && (amid <= amax)) + { + fGyr[axe].push_back(new CurveZoneControl(zone, curve, amin, amid, amax, min, init, max)); + } else { + fprintf(stderr, "incorrect gyr metadata : %s \n", fCurrentGyr.c_str()); + } + fCurrentGyr = ""; + } + + // handle screencolor metadata "...[screencolor:red|green|blue|white]..." + if (fCurrentColor.size() > 0) { + if ((fCurrentColor == "red") && (fRedReader == nullptr)) { + fRedReader = new ZoneReader(zone, min, max); + fHasScreenControl = true; + } else if ((fCurrentColor == "green") && (fGreenReader == nullptr)) { + fGreenReader = new ZoneReader(zone, min, max); + fHasScreenControl = true; + } else if ((fCurrentColor == "blue") && (fBlueReader == nullptr)) { + fBlueReader = new ZoneReader(zone, min, max); + fHasScreenControl = true; + } else if ((fCurrentColor == "white") && (fRedReader == nullptr) && (fGreenReader == nullptr) && (fBlueReader == nullptr)) { + fRedReader = new ZoneReader(zone, min, max); + fGreenReader = new ZoneReader(zone, min, max); + fBlueReader = new ZoneReader(zone, min, max); + fHasScreenControl = true; + } else { + fprintf(stderr, "incorrect screencolor metadata : %s \n", fCurrentColor.c_str()); + } + } + fCurrentColor = ""; + + fMetaData.push_back(fCurrentMetadata); + fCurrentMetadata.clear(); + } + + int getZoneIndex(std::vector* table, int p, int val) + { + FAUSTFLOAT* zone = fItems[uint(p)].fZone; + for (size_t i = 0; i < table[val].size(); i++) { + if (zone == table[val][i]->getZone()) return int(i); + } + return -1; + } + + void setConverter(std::vector* table, int p, int val, int curve, double amin, double amid, double amax) + { + int id1 = getZoneIndex(table, p, 0); + int id2 = getZoneIndex(table, p, 1); + int id3 = getZoneIndex(table, p, 2); + + // Deactivates everywhere.. + if (id1 != -1) table[0][uint(id1)]->setActive(false); + if (id2 != -1) table[1][uint(id2)]->setActive(false); + if (id3 != -1) table[2][uint(id3)]->setActive(false); + + if (val == -1) { // Means: no more mapping... + // So stay all deactivated... + } else { + int id4 = getZoneIndex(table, p, val); + if (id4 != -1) { + // Reactivate the one we edit... + table[val][uint(id4)]->setMappingValues(curve, amin, amid, amax, fItems[uint(p)].fMin, fItems[uint(p)].fInit, fItems[uint(p)].fMax); + table[val][uint(id4)]->setActive(true); + } else { + // Allocate a new CurveZoneControl which is 'active' by default + FAUSTFLOAT* zone = fItems[uint(p)].fZone; + table[val].push_back(new CurveZoneControl(zone, curve, amin, amid, amax, fItems[uint(p)].fMin, fItems[uint(p)].fInit, fItems[uint(p)].fMax)); + } + } + } + + void getConverter(std::vector* table, int p, int& val, int& curve, double& amin, double& amid, double& amax) + { + int id1 = getZoneIndex(table, p, 0); + int id2 = getZoneIndex(table, p, 1); + int id3 = getZoneIndex(table, p, 2); + + if (id1 != -1) { + val = 0; + curve = table[val][uint(id1)]->getCurve(); + table[val][uint(id1)]->getMappingValues(amin, amid, amax); + } else if (id2 != -1) { + val = 1; + curve = table[val][uint(id2)]->getCurve(); + table[val][uint(id2)]->getMappingValues(amin, amid, amax); + } else if (id3 != -1) { + val = 2; + curve = table[val][uint(id3)]->getCurve(); + table[val][uint(id3)]->getMappingValues(amin, amid, amax); + } else { + val = -1; // No mapping + curve = 0; + amin = -100.; + amid = 0.; + amax = 100.; + } + } + + public: + + APIUI() : fHasScreenControl(false), fRedReader(nullptr), fGreenReader(nullptr), fBlueReader(nullptr), fCurrentScale(kLin) + {} + + virtual ~APIUI() + { + for (const auto& it : fItems) delete it.fConversion; + for (int i = 0; i < 3; i++) { + for (const auto& it : fAcc[i]) delete it; + for (const auto& it : fGyr[i]) delete it; + } + delete fRedReader; + delete fGreenReader; + delete fBlueReader; + } + + // -- widget's layouts + + virtual void openTabBox(const char* label) { pushLabel(label); } + virtual void openHorizontalBox(const char* label) { pushLabel(label); } + virtual void openVerticalBox(const char* label) { pushLabel(label); } + virtual void closeBox() + { + if (popLabel()) { + // Shortnames can be computed when all fullnames are known + computeShortNames(); + // Fill 'shortname' field for each item + for (const auto& it : fFull2Short) { + int index = getParamIndex(it.first.c_str()); + fItems[index].fShortname = it.second; + } + } + } + + // -- active widgets + + virtual void addButton(const char* label, FAUSTFLOAT* zone) + { + addParameter(label, zone, 0, 0, 1, 1, kButton); + } + + virtual void addCheckButton(const char* label, FAUSTFLOAT* zone) + { + addParameter(label, zone, 0, 0, 1, 1, kCheckButton); + } + + virtual void addVerticalSlider(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) + { + addParameter(label, zone, init, min, max, step, kVSlider); + } + + virtual void addHorizontalSlider(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) + { + addParameter(label, zone, init, min, max, step, kHSlider); + } + + virtual void addNumEntry(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step) + { + addParameter(label, zone, init, min, max, step, kNumEntry); + } + + // -- passive widgets + + virtual void addHorizontalBargraph(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max) + { + addParameter(label, zone, min, min, max, (max-min)/1000.0f, kHBargraph); + } + + virtual void addVerticalBargraph(const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max) + { + addParameter(label, zone, min, min, max, (max-min)/1000.0f, kVBargraph); + } + + // -- soundfiles + + virtual void addSoundfile(const char* /* label */, const char* /* filename */, Soundfile** /* sf_zone */) {} + + // -- metadata declarations + + virtual void declare(FAUSTFLOAT* /* zone */, const char* key, const char* val) + { + // Keep metadata + fCurrentMetadata[key] = val; + + if (strcmp(key, "scale") == 0) { + if (strcmp(val, "log") == 0) { + fCurrentScale = kLog; + } else if (strcmp(val, "exp") == 0) { + fCurrentScale = kExp; + } else { + fCurrentScale = kLin; + } + } else if (strcmp(key, "unit") == 0) { + fCurrentUnit = val; + } else if (strcmp(key, "acc") == 0) { + fCurrentAcc = val; + } else if (strcmp(key, "gyr") == 0) { + fCurrentGyr = val; + } else if (strcmp(key, "screencolor") == 0) { + fCurrentColor = val; // val = "red", "green", "blue" or "white" + } else if (strcmp(key, "tooltip") == 0) { + fCurrentTooltip = val; + } + } + + virtual void declare(const char* /* key */, const char* /* val */) + {} + + //------------------------------------------------------------------------------- + // Simple API part + //------------------------------------------------------------------------------- + + /** + * Return the number of parameters in the UI. + * + * @return the number of parameters + */ + int getParamsCount() { return int(fItems.size()); } + + /** + * Return the param index. + * + * @param str - the UI parameter label/shortname/path + * + * @return the param index + */ + int getParamIndex(const char* str) + { + std::string path = std::string(str); + auto it = find_if(fItems.begin(), fItems.end(), + [=](const Item& it) { return (it.fLabel == path) || (it.fShortname == path) || (it.fPath == path); }); + return (it != fItems.end()) ? int(it - fItems.begin()) : -1; + } + + /** + * Return the param label. + * + * @param p - the UI parameter index + * + * @return the param label + */ + const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); } + + /** + * Return the param shortname. + * + * @param p - the UI parameter index + * + * @return the param shortname + */ + const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); } + + /** + * Return the param path. + * + * @param p - the UI parameter index + * + * @return the param path + */ + const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); } + + /** + * Return the param metadata. + * + * @param p - the UI parameter index + * + * @return the param metadata as a map + */ + std::map getMetadata(int p) + { + std::map res; + std::map metadata = fMetaData[uint(p)]; + for (const auto& it : metadata) { + res[it.first.c_str()] = it.second.c_str(); + } + return res; + } + + /** + * Return the param metadata value. + * + * @param p - the UI parameter index + * @param key - the UI parameter index + * + * @return the param metadata value associate to the key + */ + const char* getMetadata(int p, const char* key) + { + return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end()) ? fMetaData[uint(p)][key].c_str() : ""; + } + + /** + * Return the param minimum value. + * + * @param p - the UI parameter index + * + * @return the param minimum value + */ + FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; } + + /** + * Return the param maximum value. + * + * @param p - the UI parameter index + * + * @return the param maximum value + */ + FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; } + + /** + * Return the param step value. + * + * @param p - the UI parameter index + * + * @return the param step value + */ + FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; } + + /** + * Return the param init value. + * + * @param p - the UI parameter index + * + * @return the param init value + */ + FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; } + + /** + * Return the param memory zone. + * + * @param p - the UI parameter index + * + * @return the param memory zone. + */ + FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; } + + /** + * Return the param value. + * + * @param p - the UI parameter index + * + * @return the param value. + */ + FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; } + + /** + * Return the param value. + * + * @param str - the UI parameter label/shortname/path + * + * @return the param value. + */ + FAUSTFLOAT getParamValue(const char* str) + { + int index = getParamIndex(str); + if (index >= 0) { + return getParamValue(index); + } else { + fprintf(stderr, "getParamValue : '%s' not found\n", (str == nullptr ? "NULL" : str)); + return FAUSTFLOAT(0); + } + } + + /** + * Set the param value. + * + * @param p - the UI parameter index + * @param v - the UI parameter value + * + */ + void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; } + + /** + * Set the param value. + * + * @param p - the UI parameter label/shortname/path + * @param v - the UI parameter value + * + */ + void setParamValue(const char* path, FAUSTFLOAT v) + { + int index = getParamIndex(path); + if (index >= 0) { + setParamValue(index, v); + } else { + fprintf(stderr, "setParamValue : '%s' not found\n", (path == nullptr ? "NULL" : path)); + } + } + + double getParamRatio(int p) { return fItems[uint(p)].fConversion->faust2ui(*fItems[uint(p)].fZone); } + void setParamRatio(int p, double r) { *fItems[uint(p)].fZone = FAUSTFLOAT(fItems[uint(p)].fConversion->ui2faust(r)); } + + double value2ratio(int p, double r) { return fItems[uint(p)].fConversion->faust2ui(r); } + double ratio2value(int p, double r) { return fItems[uint(p)].fConversion->ui2faust(r); } + + /** + * Return the control type (kAcc, kGyr, or -1) for a given parameter. + * + * @param p - the UI parameter index + * + * @return the type + */ + Type getParamType(int p) + { + if (p >= 0) { + if (getZoneIndex(fAcc, p, 0) != -1 + || getZoneIndex(fAcc, p, 1) != -1 + || getZoneIndex(fAcc, p, 2) != -1) { + return kAcc; + } else if (getZoneIndex(fGyr, p, 0) != -1 + || getZoneIndex(fGyr, p, 1) != -1 + || getZoneIndex(fGyr, p, 2) != -1) { + return kGyr; + } + } + return kNoType; + } + + /** + * Return the Item type (kButton = 0, kCheckButton, kVSlider, kHSlider, kNumEntry, kHBargraph, kVBargraph) for a given parameter. + * + * @param p - the UI parameter index + * + * @return the Item type + */ + ItemType getParamItemType(int p) + { + return fItems[uint(p)].fItemType; + } + + /** + * Set a new value coming from an accelerometer, propagate it to all relevant FAUSTFLOAT* zones. + * + * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer + * @param value - the new value + * + */ + void propagateAcc(int acc, double value) + { + for (size_t i = 0; i < fAcc[acc].size(); i++) { + fAcc[acc][i]->update(value); + } + } + + /** + * Used to edit accelerometer curves and mapping. Set curve and related mapping for a given UI parameter. + * + * @param p - the UI parameter index + * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer (-1 means "no mapping") + * @param curve - between 0 and 3 (0: up, 1: down, 2: up and down, 2: down and up) + * @param amin - mapping 'min' point + * @param amid - mapping 'middle' point + * @param amax - mapping 'max' point + * + */ + void setAccConverter(int p, int acc, int curve, double amin, double amid, double amax) + { + setConverter(fAcc, p, acc, curve, amin, amid, amax); + } + + /** + * Used to edit gyroscope curves and mapping. Set curve and related mapping for a given UI parameter. + * + * @param p - the UI parameter index + * @param gyr - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope (-1 means "no mapping") + * @param curve - between 0 and 3 (0: up, 1: down, 2: up and down, 2: down and up) + * @param amin - mapping 'min' point + * @param amid - mapping 'middle' point + * @param amax - mapping 'max' point + * + */ + void setGyrConverter(int p, int gyr, int curve, double amin, double amid, double amax) + { + setConverter(fGyr, p, gyr, curve, amin, amid, amax); + } + + /** + * Used to edit accelerometer curves and mapping. Get curve and related mapping for a given UI parameter. + * + * @param p - the UI parameter index + * @param acc - the acc value to be retrieved (-1 means "no mapping") + * @param curve - the curve value to be retrieved (between 0 and 3) + * @param amin - the amin value to be retrieved + * @param amid - the amid value to be retrieved + * @param amax - the amax value to be retrieved + * + */ + void getAccConverter(int p, int& acc, int& curve, double& amin, double& amid, double& amax) + { + getConverter(fAcc, p, acc, curve, amin, amid, amax); + } + + /** + * Used to edit gyroscope curves and mapping. Get curve and related mapping for a given UI parameter. + * + * @param p - the UI parameter index + * @param gyr - the gyr value to be retrieved (-1 means "no mapping") + * @param curve - the curve value to be retrieved (between 0 and 3) + * @param amin - the amin value to be retrieved + * @param amid - the amid value to be retrieved + * @param amax - the amax value to be retrieved + * + */ + void getGyrConverter(int p, int& gyr, int& curve, double& amin, double& amid, double& amax) + { + getConverter(fGyr, p, gyr, curve, amin, amid, amax); + } + + /** + * Set a new value coming from an gyroscope, propagate it to all relevant FAUSTFLOAT* zones. + * + * @param gyr - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope + * @param value - the new value + * + */ + void propagateGyr(int gyr, double value) + { + for (size_t i = 0; i < fGyr[gyr].size(); i++) { + fGyr[gyr][i]->update(value); + } + } + + /** + * Get the number of FAUSTFLOAT* zones controlled with the accelerometer. + * + * @param acc - 0 for X accelerometer, 1 for Y accelerometer, 2 for Z accelerometer + * @return the number of zones + * + */ + int getAccCount(int acc) + { + return (acc >= 0 && acc < 3) ? int(fAcc[acc].size()) : 0; + } + + /** + * Get the number of FAUSTFLOAT* zones controlled with the gyroscope. + * + * @param gyr - 0 for X gyroscope, 1 for Y gyroscope, 2 for Z gyroscope + * @param the number of zones + * + */ + int getGyrCount(int gyr) + { + return (gyr >= 0 && gyr < 3) ? int(fGyr[gyr].size()) : 0; + } + + /** + * Get the requested screen color. + * + * -1 means no screen color control (no screencolor metadata found) + * otherwise return 0x00RRGGBB a ready to use color + * + */ + int getScreenColor() + { + if (fHasScreenControl) { + int r = (fRedReader) ? fRedReader->getValue() : 0; + int g = (fGreenReader) ? fGreenReader->getValue() : 0; + int b = (fBlueReader) ? fBlueReader->getValue() : 0; + return (r<<16) | (g<<8) | b; + } else { + return -1; + } + } + +}; + +#endif +/************************** END APIUI.h **************************/ + +// NOTE: "faust -scn name" changes the last line above to +// #include + +//---------------------------------------------------------------------------- +// FAUST Generated Code +//---------------------------------------------------------------------------- + + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#include +#include +#include + +#ifndef FAUSTCLASS +#define FAUSTCLASS stereotomonodsp +#endif + +#ifdef __APPLE__ +#define exp10f __exp10f +#define exp10 __exp10 +#endif + +#if defined(_WIN32) +#define RESTRICT __restrict +#else +#define RESTRICT __restrict__ +#endif + + +class stereotomonodsp : public dsp { + + private: + + int fSampleRate; + + public: + + void metadata(Meta* m) { + m->declare("author", "Dominick Hing"); + m->declare("compile_options", "-a faust2header.cpp -lang cpp -i -inpl -cn stereotomonodsp -es 1 -mcd 16 -single -ftz 0"); + m->declare("description", "Stereo-to-Mono Faust Plugin for JackTrip"); + m->declare("filename", "stereotomonodsp.dsp"); + m->declare("license", "MIT Style STK-4.2"); + m->declare("name", "stereo-to-mono"); + m->declare("version", "1.0"); + } + + virtual int getNumInputs() { + return 2; + } + virtual int getNumOutputs() { + return 1; + } + + static void classInit(int /* sample_rate */) { + } + + virtual void instanceConstants(int sample_rate) { + fSampleRate = sample_rate; + } + + virtual void instanceResetUserInterface() { + } + + virtual void instanceClear() { + } + + virtual void init(int sample_rate) { + classInit(sample_rate); + instanceInit(sample_rate); + } + virtual void instanceInit(int sample_rate) { + instanceConstants(sample_rate); + instanceResetUserInterface(); + instanceClear(); + } + + virtual stereotomonodsp* clone() { + return new stereotomonodsp(); + } + + virtual int getSampleRate() { + return fSampleRate; + } + + virtual void buildUserInterface(UI* ui_interface) { + ui_interface->openVerticalBox("stereo-to-mono"); + ui_interface->closeBox(); + } + + virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs) { + FAUSTFLOAT* input0 = inputs[0]; + FAUSTFLOAT* input1 = inputs[1]; + FAUSTFLOAT* output0 = outputs[0]; + for (int i0 = 0; i0 < count; i0 = i0 + 1) { + float fTemp0 = float(input0[i0]); + float fTemp1 = float(input1[i0]); + output0[i0] = FAUSTFLOAT(0.5f * (fTemp0 + fTemp1)); + } + } + +}; + + +#endif